diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 15a15b2..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 8567414..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 2326c24..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 194b1f0..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 643a114..70a7d10 100644 --- a/README.md +++ b/README.md @@ -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'". diff --git a/app/build.gradle b/app/build.gradle index d5e0dd5..e5189a2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/androidTest/java/org/mercury_im/messenger/RequeryDatabaseTest.java b/app/src/androidTest/java/org/mercury_im/messenger/RequeryDatabaseTest.java index 32ea850..44379e3 100644 --- a/app/src/androidTest/java/org/mercury_im/messenger/RequeryDatabaseTest.java +++ b/app/src/androidTest/java/org/mercury_im/messenger/RequeryDatabaseTest.java @@ -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; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 37827cb..46c12f2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,9 +33,9 @@ android:name=".ui.settings.SettingsActivity" android:label="@string/title_activity_settings" /> - + \ No newline at end of file diff --git a/app/src/main/java/org/mercury_im/messenger/ClientStateHandler.java b/app/src/main/java/org/mercury_im/messenger/ClientStateHandler.java new file mode 100644 index 0000000..7d4426c --- /dev/null +++ b/app/src/main/java/org/mercury_im/messenger/ClientStateHandler.java @@ -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 { + 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 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); + } } diff --git a/app/src/main/java/org/mercury_im/messenger/Notifications.java b/app/src/main/java/org/mercury_im/messenger/Notifications.java index 702ab1f..823d844 100644 --- a/app/src/main/java/org/mercury_im/messenger/Notifications.java +++ b/app/src/main/java/org/mercury_im/messenger/Notifications.java @@ -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(); } } diff --git a/app/src/main/java/org/mercury_im/messenger/ParcelableXMPPTCPConnectionConfiguration.java b/app/src/main/java/org/mercury_im/messenger/ParcelableXMPPTCPConnectionConfiguration.java deleted file mode 100644 index e59013f..0000000 --- a/app/src/main/java/org/mercury_im/messenger/ParcelableXMPPTCPConnectionConfiguration.java +++ /dev/null @@ -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 CREATOR = new Creator() { - @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; - } -} diff --git a/app/src/main/java/org/mercury_im/messenger/di/component/AppComponent.java b/app/src/main/java/org/mercury_im/messenger/di/component/AppComponent.java index 939f4cb..ca69c42 100644 --- a/app/src/main/java/org/mercury_im/messenger/di/component/AppComponent.java +++ b/app/src/main/java/org/mercury_im/messenger/di/component/AppComponent.java @@ -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); } diff --git a/app/src/main/java/org/mercury_im/messenger/di/module/AndroidPersistenceModule.java b/app/src/main/java/org/mercury_im/messenger/di/module/AndroidPersistenceModule.java index 1598587..05ad91a 100644 --- a/app/src/main/java/org/mercury_im/messenger/di/module/AndroidPersistenceModule.java +++ b/app/src/main/java/org/mercury_im/messenger/di/module/AndroidPersistenceModule.java @@ -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 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 dataStore = ReactiveSupport.toReactiveStore( - new EntityDataStore<>(configuration)); - return dataStore; + return ReactiveSupport.toReactiveStore(new EntityDataStore<>(configuration)); } @Provides diff --git a/app/src/main/java/org/mercury_im/messenger/di/module/AppModule.java b/app/src/main/java/org/mercury_im/messenger/di/module/AppModule.java index 81e8491..d2fceee 100644 --- a/app/src/main/java/org/mercury_im/messenger/di/module/AppModule.java +++ b/app/src/main/java/org/mercury_im/messenger/di/module/AppModule.java @@ -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; - } } diff --git a/app/src/main/java/org/mercury_im/messenger/service/XmppConnectionService.java b/app/src/main/java/org/mercury_im/messenger/service/MercuryConnectionService.java similarity index 59% rename from app/src/main/java/org/mercury_im/messenger/service/XmppConnectionService.java rename to app/src/main/java/org/mercury_im/messenger/service/MercuryConnectionService.java index 8f5ce85..6be80c3 100644 --- a/app/src/main/java/org/mercury_im/messenger/service/XmppConnectionService.java +++ b/app/src/main/java/org/mercury_im/messenger/service/MercuryConnectionService.java @@ -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; } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/MainActivity.java b/app/src/main/java/org/mercury_im/messenger/ui/MainActivity.java index 7ec36ee..d8a3bad 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/MainActivity.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/MainActivity.java @@ -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(); } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/login/AccountsFragment.java b/app/src/main/java/org/mercury_im/messenger/ui/account/AccountsFragment.java similarity index 77% rename from app/src/main/java/org/mercury_im/messenger/ui/login/AccountsFragment.java rename to app/src/main/java/org/mercury_im/messenger/ui/account/AccountsFragment.java index 29f11b7..84da9b0 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/login/AccountsFragment.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/account/AccountsFragment.java @@ -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 for more information. */ public interface OnAccountListItemClickListener { - void onAccountListItemClick(AccountModel item); + void onAccountListItemClick(Account item); - void onAccountListItemLongClick(AccountModel item); + void onAccountListItemLongClick(Account item); } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/account/AccountsRecyclerViewAdapter.java b/app/src/main/java/org/mercury_im/messenger/ui/account/AccountsRecyclerViewAdapter.java new file mode 100644 index 0000000..97dc0e9 --- /dev/null +++ b/app/src/main/java/org/mercury_im/messenger/ui/account/AccountsRecyclerViewAdapter.java @@ -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 { + + private final List 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 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 { + + public AccountsDiffCallback(List newItems, List 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); + } + } +} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/account/AccountsViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/account/AccountsViewModel.java new file mode 100644 index 0000000..09b0ffb --- /dev/null +++ b/app/src/main/java/org/mercury_im/messenger/ui/account/AccountsViewModel.java @@ -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> 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> getConnections() { + return connections; + } + + public void setAccountEnabled(Account accountModel, boolean enabled) { + accountModel.setEnabled(enabled); + repository.upsertAccount(accountModel) + .subscribe(); + } +} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/account/LoginActivity.java b/app/src/main/java/org/mercury_im/messenger/ui/account/LoginActivity.java new file mode 100644 index 0000000..300f9d3 --- /dev/null +++ b/app/src/main/java/org/mercury_im/messenger/ui/account/LoginActivity.java @@ -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 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 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; + } +} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/account/LoginViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/account/LoginViewModel.java new file mode 100644 index 0000000..29a8245 --- /dev/null +++ b/app/src/main/java/org/mercury_im/messenger/ui/account/LoginViewModel.java @@ -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 usernameError = new MutableLiveData<>(new Error()); + private MutableLiveData passwordError = new MutableLiveData<>(new Error()); + private MutableLiveData loginButtonEnabled = new MutableLiveData<>(false); + private MutableLiveData 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 getPasswordError() { + return passwordError; + } + + public LiveData getUsernameError() { + return usernameError; + } + + public LiveData isLoginSuccessful() { + return loginCompleted; + } + + public LiveData 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; + } + } +} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/login/package-info.java b/app/src/main/java/org/mercury_im/messenger/ui/account/package-info.java similarity index 55% rename from app/src/main/java/org/mercury_im/messenger/ui/login/package-info.java rename to app/src/main/java/org/mercury_im/messenger/ui/account/package-info.java index 39d6812..270cacb 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/login/package-info.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/account/package-info.java @@ -1,4 +1,4 @@ /** * Some Javadoc information about the package. */ -package org.mercury_im.messenger.ui.login; +package org.mercury_im.messenger.ui.account; diff --git a/app/src/main/java/org/mercury_im/messenger/ui/avatar/AvatarDrawable.java b/app/src/main/java/org/mercury_im/messenger/ui/avatar/AvatarDrawable.java new file mode 100644 index 0000000..e014ad4 --- /dev/null +++ b/app/src/main/java/org/mercury_im/messenger/ui/avatar/AvatarDrawable.java @@ -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 Ltt.rs for Android + */ +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); + } +} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatActivity.java b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatActivity.java index 02b24b5..a6e2064 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatActivity.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatActivity.java @@ -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 diff --git a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatInputFragment.java b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatInputFragment.java index 9a02bff..1de0c9f 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatInputFragment.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatInputFragment.java @@ -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: diff --git a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatViewModel.java index 5629431..626c1f3 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatViewModel.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/chat/ChatViewModel.java @@ -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 entity = new MutableLiveData<>(); - private MutableLiveData contact = new MutableLiveData<>(); - private MutableLiveData> messages = new MutableLiveData<>(); + private MutableLiveData contact = new MutableLiveData<>(); + private MutableLiveData> messages = new MutableLiveData<>(); private MutableLiveData contactDisplayName = new MutableLiveData<>(); - private MutableLiveData chat = new MutableLiveData<>(); + private MutableLiveData 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) this::init)); + public void init(UUID accountId, EntityBareJid jid) { + disposable.add(contactRepository.getOrCreatePeer(accountId, jid.toString()) + .subscribe((Consumer) this::init)); } - public void init(EntityModel entityModel) { + public void init(Peer peer) { + disposable.add(chatRepository.getOrCreateChatWithPeer(peer) + .subscribe((Consumer) 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 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> getMessages() { + public LiveData> getMessages() { return messages; } - public LiveData getContact() { + public LiveData 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>) - messages -> ChatViewModel.this.messages.setValue(messages))); - } - disposable.add(messageRepository.findMessageByQuery(accountId, jid, query) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((Consumer>) o -> { - messages.setValue(o); - })); + Observable> 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; } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/chat/MessagesRecyclerViewAdapter.java b/app/src/main/java/org/mercury_im/messenger/ui/chat/MessagesRecyclerViewAdapter.java index 2bac3f8..09da304 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/chat/MessagesRecyclerViewAdapter.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/chat/MessagesRecyclerViewAdapter.java @@ -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 { - private List messages = new ArrayList<>(); - private SparseArray checkedItems = new SparseArray<>(); + private List messages = new ArrayList<>(); + private SparseBooleanArray checkedItems = new SparseBooleanArray(); public MessagesRecyclerViewAdapter() { } - public void updateMessages(List messages) { + public void updateMessages(List messages) { this.messages.clear(); this.messages.addAll(messages); notifyDataSetChanged(); @@ -47,14 +48,14 @@ public class MessagesRecyclerViewAdapter extends RecyclerView.Adapter { - 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); }); } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListRecyclerViewAdapter.java b/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListRecyclerViewAdapter.java index a29c9e0..13a1e1f 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListRecyclerViewAdapter.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListRecyclerViewAdapter.java @@ -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 { + extends AbstractRecyclerViewAdapter { 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 { + private static class ChatMessageDiffCallback extends AbstractDiffCallback { 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()); } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListViewModel.java index 1e06355..cd7be0b 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListViewModel.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/chatlist/ChatListViewModel.java @@ -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> chats = new MutableLiveData<>(); + private final MutableLiveData> 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> getChats() { + public LiveData> getChats() { return chats; } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/login/AccountsRecyclerViewAdapter.java b/app/src/main/java/org/mercury_im/messenger/ui/login/AccountsRecyclerViewAdapter.java deleted file mode 100644 index 561687d..0000000 --- a/app/src/main/java/org/mercury_im/messenger/ui/login/AccountsRecyclerViewAdapter.java +++ /dev/null @@ -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 { - - private final List 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 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 { - - public AccountsDiffCallback(List newItems, List 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); - } - } -} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/login/AccountsViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/login/AccountsViewModel.java deleted file mode 100644 index 9436a94..0000000 --- a/app/src/main/java/org/mercury_im/messenger/ui/login/AccountsViewModel.java +++ /dev/null @@ -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> 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> 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(); - } -} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/login/AddAccountDialog.java b/app/src/main/java/org/mercury_im/messenger/ui/login/AddAccountDialog.java deleted file mode 100644 index eb276cc..0000000 --- a/app/src/main/java/org/mercury_im/messenger/ui/login/AddAccountDialog.java +++ /dev/null @@ -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); - - } - -} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/login/LoginActivity.java b/app/src/main/java/org/mercury_im/messenger/ui/login/LoginActivity.java deleted file mode 100644 index 5e07325..0000000 --- a/app/src/main/java/org/mercury_im/messenger/ui/login/LoginActivity.java +++ /dev/null @@ -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 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; - } -} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/login/LoginViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/login/LoginViewModel.java deleted file mode 100644 index 0420d28..0000000 --- a/app/src/main/java/org/mercury_im/messenger/ui/login/LoginViewModel.java +++ /dev/null @@ -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 = new MutableLiveData<>(); - private MutableLiveData passwordError = new MutableLiveData<>(); - - private MutableLiveData account = new MutableLiveData<>(); - - private MutableLiveData signinSuccessful = new MutableLiveData<>(); - - public LoginViewModel() { - super(); - MercuryImApplication.getApplication().getAppComponent().inject(this); - init(new AccountModel()); - } - - public LiveData 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 getJidError() { - return jidError; - } - - public LiveData 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 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 insert = accountRepository.upsert(accountModel); - insert.subscribe(new DisposableSingleObserver() { - @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); - } - }); - - } - } -} diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListFragment.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListFragment.java index 8115d00..d12eece 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListFragment.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListFragment.java @@ -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; diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListItemViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListItemViewModel.java index bfa85f6..ea26a71 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListItemViewModel.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListItemViewModel.java @@ -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 contact; + private LiveData contact; @Inject public ContactListItemViewModel(@NonNull Application application) { diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListRecyclerViewAdapter.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListRecyclerViewAdapter.java index 515070f..584f866 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListRecyclerViewAdapter.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListRecyclerViewAdapter.java @@ -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 { + extends AbstractRecyclerViewAdapter { 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 { + private static class ContactDiffCallback extends AbstractDiffCallback { 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()); } } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListViewModel.java b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListViewModel.java index c50666e..86c87fc 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListViewModel.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/roster/contacts/ContactListViewModel.java @@ -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> rosterEntryList = new MutableLiveData<>(); + private final MutableLiveData> 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 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> getRosterEntryList() { + public LiveData> getRosterEntryList() { return rosterEntryList; } } diff --git a/app/src/main/java/org/mercury_im/messenger/ui/settings/SettingsActivity.java b/app/src/main/java/org/mercury_im/messenger/ui/settings/SettingsActivity.java index 0ea25a9..59a99a0 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/settings/SettingsActivity.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/settings/SettingsActivity.java @@ -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) diff --git a/app/src/main/java/org/mercury_im/messenger/ui/util/AbstractRecyclerViewAdapter.java b/app/src/main/java/org/mercury_im/messenger/ui/util/AbstractRecyclerViewAdapter.java index 2b23247..8301ff9 100644 --- a/app/src/main/java/org/mercury_im/messenger/ui/util/AbstractRecyclerViewAdapter.java +++ b/app/src/main/java/org/mercury_im/messenger/ui/util/AbstractRecyclerViewAdapter.java @@ -27,7 +27,7 @@ public abstract class AbstractRecyclerViewAdapter + tools:context=".ui.account.LoginActivity"> Password Sign in Sign in - This XMPP address is invalid + This XMPP address is invalid This password is too short This password is incorrect This field is required diff --git a/build.gradle b/build.gradle index bc43b8e..ba1be77 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/core/.gitignore b/core-old/.gitignore similarity index 100% rename from core/.gitignore rename to core-old/.gitignore diff --git a/core/build.gradle b/core-old/build.gradle similarity index 96% rename from core/build.gradle rename to core-old/build.gradle index 43bb7fc..348a34c 100644 --- a/core/build.gradle +++ b/core-old/build.gradle @@ -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" diff --git a/core-old/src/main/java/org/mercury_im/domain/data/util/ContactNameUtil.java b/core-old/src/main/java/org/mercury_im/domain/data/util/ContactNameUtil.java new file mode 100644 index 0000000..b4159f3 --- /dev/null +++ b/core-old/src/main/java/org/mercury_im/domain/data/util/ContactNameUtil.java @@ -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(); + } +} diff --git a/core/src/main/java/org/mercury_im/messenger/core/NotificationManager.java b/core-old/src/main/java/org/mercury_im/messenger/core/NotificationManager.java similarity index 70% rename from core/src/main/java/org/mercury_im/messenger/core/NotificationManager.java rename to core-old/src/main/java/org/mercury_im/messenger/core/NotificationManager.java index d5f4f48..c645fd4 100644 --- a/core/src/main/java/org/mercury_im/messenger/core/NotificationManager.java +++ b/core-old/src/main/java/org/mercury_im/messenger/core/NotificationManager.java @@ -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 { diff --git a/core/src/main/java/org/mercury_im/messenger/core/centers/ConnectionCenter.java b/core-old/src/main/java/org/mercury_im/messenger/core/centers/ConnectionCenter.java similarity index 96% rename from core/src/main/java/org/mercury_im/messenger/core/centers/ConnectionCenter.java rename to core-old/src/main/java/org/mercury_im/messenger/core/centers/ConnectionCenter.java index 491daa0..3efd65e 100644 --- a/core/src/main/java/org/mercury_im/messenger/core/centers/ConnectionCenter.java +++ b/core-old/src/main/java/org/mercury_im/messenger/core/centers/ConnectionCenter.java @@ -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; diff --git a/core/src/main/java/org/mercury_im/messenger/core/centers/ContactCenter.java b/core-old/src/main/java/org/mercury_im/messenger/core/centers/ContactCenter.java similarity index 100% rename from core/src/main/java/org/mercury_im/messenger/core/centers/ContactCenter.java rename to core-old/src/main/java/org/mercury_im/messenger/core/centers/ContactCenter.java diff --git a/core/src/main/java/org/mercury_im/messenger/core/centers/MessageCenter.java b/core-old/src/main/java/org/mercury_im/messenger/core/centers/MessageCenter.java similarity index 100% rename from core/src/main/java/org/mercury_im/messenger/core/centers/MessageCenter.java rename to core-old/src/main/java/org/mercury_im/messenger/core/centers/MessageCenter.java diff --git a/core/src/main/java/org/mercury_im/messenger/core/connection/ConnectionState.java b/core-old/src/main/java/org/mercury_im/messenger/core/connection/ConnectionState.java similarity index 100% rename from core/src/main/java/org/mercury_im/messenger/core/connection/ConnectionState.java rename to core-old/src/main/java/org/mercury_im/messenger/core/connection/ConnectionState.java diff --git a/core/src/main/java/org/mercury_im/messenger/core/connection/MercuryConfiguration.java b/core-old/src/main/java/org/mercury_im/messenger/core/connection/MercuryConfiguration.java similarity index 100% rename from core/src/main/java/org/mercury_im/messenger/core/connection/MercuryConfiguration.java rename to core-old/src/main/java/org/mercury_im/messenger/core/connection/MercuryConfiguration.java diff --git a/core/src/main/java/org/mercury_im/messenger/core/connection/MercuryConnection.java b/core-old/src/main/java/org/mercury_im/messenger/core/connection/MercuryConnection.java similarity index 100% rename from core/src/main/java/org/mercury_im/messenger/core/connection/MercuryConnection.java rename to core-old/src/main/java/org/mercury_im/messenger/core/connection/MercuryConnection.java diff --git a/core/src/main/java/org/mercury_im/messenger/core/di/CenterModule.java b/core-old/src/main/java/org/mercury_im/messenger/core/di/CenterModule.java similarity index 80% rename from core/src/main/java/org/mercury_im/messenger/core/di/CenterModule.java rename to core-old/src/main/java/org/mercury_im/messenger/core/di/CenterModule.java index 3e445f1..4c9f2a4 100644 --- a/core/src/main/java/org/mercury_im/messenger/core/di/CenterModule.java +++ b/core-old/src/main/java/org/mercury_im/messenger/core/di/CenterModule.java @@ -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; diff --git a/core/src/main/java/org/mercury_im/messenger/core/di/XmppComponent.java b/core-old/src/main/java/org/mercury_im/messenger/core/di/XmppComponent.java similarity index 78% rename from core/src/main/java/org/mercury_im/messenger/core/di/XmppComponent.java rename to core-old/src/main/java/org/mercury_im/messenger/core/di/XmppComponent.java index a959cfc..4497b59 100644 --- a/core/src/main/java/org/mercury_im/messenger/core/di/XmppComponent.java +++ b/core-old/src/main/java/org/mercury_im/messenger/core/di/XmppComponent.java @@ -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; diff --git a/core/src/main/java/org/mercury_im/messenger/core/stores/EntityCapsStore.java b/core-old/src/main/java/org/mercury_im/messenger/core/stores/EntityCapsStore.java similarity index 93% rename from core/src/main/java/org/mercury_im/messenger/core/stores/EntityCapsStore.java rename to core-old/src/main/java/org/mercury_im/messenger/core/stores/EntityCapsStore.java index 7a1bcff..ff67a9d 100644 --- a/core/src/main/java/org/mercury_im/messenger/core/stores/EntityCapsStore.java +++ b/core-old/src/main/java/org/mercury_im/messenger/core/stores/EntityCapsStore.java @@ -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 discoverInfoMap = new HashMap<>(); private final CompositeDisposable disposable = new CompositeDisposable(); @Inject - public EntityCapsStore(EntityCapsRepository entityCapsRepository) { + public EntityCapsStore(XmppEntityCapsRepository entityCapsRepository) { this.entityCapsRepository = entityCapsRepository; populateFromDatabase(); } diff --git a/core/src/main/java/org/mercury_im/messenger/core/stores/PlainMessageStore.java b/core-old/src/main/java/org/mercury_im/messenger/core/stores/PlainMessageStore.java similarity index 93% rename from core/src/main/java/org/mercury_im/messenger/core/stores/PlainMessageStore.java rename to core-old/src/main/java/org/mercury_im/messenger/core/stores/PlainMessageStore.java index b72ad47..87f3ada 100644 --- a/core/src/main/java/org/mercury_im/messenger/core/stores/PlainMessageStore.java +++ b/core-old/src/main/java/org/mercury_im/messenger/core/stores/PlainMessageStore.java @@ -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; diff --git a/core/src/main/java/org/mercury_im/messenger/core/stores/RosterStore.java b/core-old/src/main/java/org/mercury_im/messenger/core/stores/RosterStore.java similarity index 92% rename from core/src/main/java/org/mercury_im/messenger/core/stores/RosterStore.java rename to core-old/src/main/java/org/mercury_im/messenger/core/stores/RosterStore.java index c1843c4..5966ec4 100644 --- a/core/src/main/java/org/mercury_im/messenger/core/stores/RosterStore.java +++ b/core-old/src/main/java/org/mercury_im/messenger/core/stores/RosterStore.java @@ -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; } diff --git a/core/src/main/java/org/mercury_im/messenger/core/util/ContactNameUtil.java b/core/src/main/java/org/mercury_im/messenger/core/util/ContactNameUtil.java deleted file mode 100644 index cb2893e..0000000 --- a/core/src/main/java/org/mercury_im/messenger/core/util/ContactNameUtil.java +++ /dev/null @@ -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); - } -} diff --git a/persistence/.gitignore b/data/.gitignore similarity index 100% rename from persistence/.gitignore rename to data/.gitignore diff --git a/persistence/build.gradle b/data/build.gradle similarity index 61% rename from persistence/build.gradle rename to data/build.gradle index a44cfb2..af39365 100644 --- a/persistence/build.gradle +++ b/data/build.gradle @@ -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" diff --git a/data/src/main/java/org/mercury_im/messenger/data/converter/MessageDirectionConverter.java b/data/src/main/java/org/mercury_im/messenger/data/converter/MessageDirectionConverter.java new file mode 100644 index 0000000..412b063 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/converter/MessageDirectionConverter.java @@ -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 { + @Override + public Class getMappedType() { + return MessageDirection.class; + } + + @Override + public Class 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 type, Integer value) { + switch (value) { + case 0: + return MessageDirection.outgoing; + case 1: + return MessageDirection.incoming; + default: return MessageDirection.incoming; + } + } +} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/converter/SaslConditionConverter.java b/data/src/main/java/org/mercury_im/messenger/data/converter/SaslConditionConverter.java similarity index 85% rename from persistence/src/main/java/org/mercury_im/messenger/persistence/converter/SaslConditionConverter.java rename to data/src/main/java/org/mercury_im/messenger/data/converter/SaslConditionConverter.java index 1de9950..0b93393 100644 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/converter/SaslConditionConverter.java +++ b/data/src/main/java/org/mercury_im/messenger/data/converter/SaslConditionConverter.java @@ -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; diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/converter/SubscriptionDirectionConverter.java b/data/src/main/java/org/mercury_im/messenger/data/converter/SubscriptionDirectionConverter.java similarity index 86% rename from persistence/src/main/java/org/mercury_im/messenger/persistence/converter/SubscriptionDirectionConverter.java rename to data/src/main/java/org/mercury_im/messenger/data/converter/SubscriptionDirectionConverter.java index 5e23414..b8ecf7f 100644 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/converter/SubscriptionDirectionConverter.java +++ b/data/src/main/java/org/mercury_im/messenger/data/converter/SubscriptionDirectionConverter.java @@ -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; diff --git a/data/src/main/java/org/mercury_im/messenger/data/di/DaoModule.java b/data/src/main/java/org/mercury_im/messenger/data/di/DaoModule.java new file mode 100644 index 0000000..3b472d1 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/di/DaoModule.java @@ -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 data) { + return new AccountDao(data); + } + + @Provides + @Singleton + static DirectChatDao provideDirectChatDao(ReactiveEntityStore data) { + return new DirectChatDao(data); + } + + @Provides + @Singleton + static GroupChatDao provideGroupChatDao(ReactiveEntityStore data) { + return new GroupChatDao(data); + } + + @Provides + @Singleton + static MessageDao provideMessageDao(ReactiveEntityStore data) { + return new MessageDao(data); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/di/MappingModule.java b/data/src/main/java/org/mercury_im/messenger/data/di/MappingModule.java new file mode 100644 index 0000000..e1aa7d8 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/di/MappingModule.java @@ -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(); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/di/RepositoryModule.java b/data/src/main/java/org/mercury_im/messenger/data/di/RepositoryModule.java new file mode 100644 index 0000000..00875cb --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/di/RepositoryModule.java @@ -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 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 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 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 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 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 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); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/enums/MessageContentType.java b/data/src/main/java/org/mercury_im/messenger/data/enums/MessageContentType.java new file mode 100644 index 0000000..19367eb --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/enums/MessageContentType.java @@ -0,0 +1,8 @@ +package org.mercury_im.messenger.data.enums; + +public enum MessageContentType { + /** + * Content is a message body. + */ + body +} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/enums/SaslCondition.java b/data/src/main/java/org/mercury_im/messenger/data/enums/SaslCondition.java similarity index 94% rename from persistence/src/main/java/org/mercury_im/messenger/persistence/enums/SaslCondition.java rename to data/src/main/java/org/mercury_im/messenger/data/enums/SaslCondition.java index 43026a6..da381d0 100644 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/enums/SaslCondition.java +++ b/data/src/main/java/org/mercury_im/messenger/data/enums/SaslCondition.java @@ -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
element or an
element with * initial response data. * - * @see rfc6120 §6.5.6: invalid-authzid + * @see rfc6120 §6.5.6: invalidUsername-authzid */ 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
element. * - * @see rfc6120 §6.5.7: invalid-mechanism + * @see rfc6120 §6.5.7: invalidUsername-mechanism */ invalid_mechanism, diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/AbstractMapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/AbstractMapping.java new file mode 100644 index 0000000..b64142d --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/AbstractMapping.java @@ -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 implements Mapping { + + @Override + public M toModel(E entity) { + if (entity == null) { + return null; + } + return toModel(entity, newModel(entity)); + } + + @Override + public Optional toModel(Optional 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 toEntity(Optional 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); +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/AccountMapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/AccountMapping.java new file mode 100644 index 0000000..88bd641 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/AccountMapping.java @@ -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 { + + @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; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/DirectChatMapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/DirectChatMapping.java new file mode 100644 index 0000000..54f1017 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/DirectChatMapping.java @@ -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 { + + 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; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/EntityCapsMapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/EntityCapsMapping.java new file mode 100644 index 0000000..7c04840 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/EntityCapsMapping.java @@ -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 { + + @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; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/GroupChatMapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/GroupChatMapping.java new file mode 100644 index 0000000..0e92d5f --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/GroupChatMapping.java @@ -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 { + + 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; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/Mapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/Mapping.java new file mode 100644 index 0000000..922facc --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/Mapping.java @@ -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 Entity + * @param Model + */ +public interface Mapping { + + /** + * Map data from the entity to a new model. + * + * @param entity application entity + * @return new database model + */ + M toModel(E entity); + + Optional toModel(Optional entity); + + /** + * Copy data from the model to a new entity. + * @param model database model + * @return new application entity + */ + E toEntity(M model); + + Optional toEntity(Optional 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); + + +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/MessageMapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/MessageMapping.java new file mode 100644 index 0000000..6b12c60 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/MessageMapping.java @@ -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 { + + 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 payloadContainers = new ArrayList<>(entity.getMessagePayloads().size()); + for (MessagePayloadContainerModel containerModel : model.getPayloads()) { + payloadContainers.add(messagePayloadContainerMapping.toEntity(containerModel)); + } + entity.setMessagePayloads(payloadContainers); + return entity; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/MessagePayloadContainerMapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/MessagePayloadContainerMapping.java new file mode 100644 index 0000000..4c06ab3 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/MessagePayloadContainerMapping.java @@ -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 { + + 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 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; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/MessagePayloadMapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/MessagePayloadMapping.java new file mode 100644 index 0000000..3328dc9 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/MessagePayloadMapping.java @@ -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 { + + @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; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/PeerMapping.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/PeerMapping.java new file mode 100644 index 0000000..864dc98 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/PeerMapping.java @@ -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 { + + 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; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/mapping/package-info.java b/data/src/main/java/org/mercury_im/messenger/data/mapping/package-info.java new file mode 100644 index 0000000..83aa1c8 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/mapping/package-info.java @@ -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
entity
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; diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractAccountModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractAccountModel.java similarity index 65% rename from persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractAccountModel.java rename to data/src/main/java/org/mercury_im/messenger/data/model/AbstractAccountModel.java index 5af7a4c..126693b 100644 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractAccountModel.java +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractAccountModel.java @@ -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") + "]"; } } diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractDirectChatModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractDirectChatModel.java new file mode 100644 index 0000000..a79a2bc --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractDirectChatModel.java @@ -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; +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractDirectMessagesRelation.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractDirectMessagesRelation.java new file mode 100644 index 0000000..6ecbf2f --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractDirectMessagesRelation.java @@ -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; +} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractEntityCapsModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractEntityCapsModel.java similarity index 85% rename from persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractEntityCapsModel.java rename to data/src/main/java/org/mercury_im/messenger/data/model/AbstractEntityCapsModel.java index fa3b20e..165f568 100644 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractEntityCapsModel.java +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractEntityCapsModel.java @@ -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; diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractGroupChatModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractGroupChatModel.java new file mode 100644 index 0000000..ae103f8 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractGroupChatModel.java @@ -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; +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractGroupMessagesRelation.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractGroupMessagesRelation.java new file mode 100644 index 0000000..f673a09 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractGroupMessagesRelation.java @@ -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; +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractMessageModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractMessageModel.java new file mode 100644 index 0000000..279c07c --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractMessageModel.java @@ -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 payloads; + + @Column + String legacyId; + + @Column + String originId; + + @Column + String stanzaId; + + @Column + String stanzaIdBy; +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractMessagePayloadContainerModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractMessagePayloadContainerModel.java new file mode 100644 index 0000000..8bd4a74 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractMessagePayloadContainerModel.java @@ -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 contents; +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractMessagePayloadModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractMessagePayloadModel.java new file mode 100644 index 0000000..00a7d76 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractMessagePayloadModel.java @@ -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; +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/AbstractPeerModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractPeerModel.java new file mode 100644 index 0000000..5a4e13e --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractPeerModel.java @@ -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 + "]"; + } +} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractSaslAuthenticationResultModel.java b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractSaslAuthenticationResultModel.java similarity index 75% rename from persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractSaslAuthenticationResultModel.java rename to data/src/main/java/org/mercury_im/messenger/data/model/AbstractSaslAuthenticationResultModel.java index 1753071..f13b416 100644 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractSaslAuthenticationResultModel.java +++ b/data/src/main/java/org/mercury_im/messenger/data/model/AbstractSaslAuthenticationResultModel.java @@ -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; diff --git a/data/src/main/java/org/mercury_im/messenger/data/model/package-info.java b/data/src/main/java/org/mercury_im/messenger/data/model/package-info.java new file mode 100644 index 0000000..77cc9dd --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/model/package-info.java @@ -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 + *
build/generated/sources/annotationProcessor/java/main/...
. + * + * The structure of the model classes closely mimics the structure of their entity pendants + * declared in the
entity
module. + * + * @see requery wiki on model definitions + */ +package org.mercury_im.messenger.data.model; diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/RequeryRepository.java b/data/src/main/java/org/mercury_im/messenger/data/repository/RequeryRepository.java similarity index 70% rename from persistence/src/main/java/org/mercury_im/messenger/persistence/repository/RequeryRepository.java rename to data/src/main/java/org/mercury_im/messenger/data/repository/RequeryRepository.java index 4cf203d..8b814b2 100644 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/RequeryRepository.java +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/RequeryRepository.java @@ -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 data; protected RequeryRepository(ReactiveEntityStore 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; diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/XmppAccountRepository.java b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppAccountRepository.java new file mode 100644 index 0000000..f683023 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppAccountRepository.java @@ -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 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 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> observeAccount(UUID accountId) { + return dao.get(accountId).observableResult() + .map(result -> new Optional<>(result.firstOrNull())) + .map(accountMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Maybe getAccount(UUID accountId) { + return dao.get(accountId).maybe() + .map(accountMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeAccountByAddress(String address) { + return dao.get(address).observableResult() + .map(result -> new Optional<>(result.firstOrNull())) + .map(accountMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Maybe getAccountByAddress(String address) { + return dao.get(address).maybe() + .map(accountMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeAllAccounts() { + return dao.getAll().observableResult() + .map(ResultDelegate::toList) + .map(this::modelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable observeAccounts() { + return dao.getAll().observableResult() + .flatMap(ReactiveResult::observable) + .map(accountMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Single 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 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 modelsToEntities(List models) { + List entities = new ArrayList<>(models.size()); + for (AccountModel model : models) { + entities.add(accountMapping.toEntity(model)); + } + return entities; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/XmppDirectChatRepository.java b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppDirectChatRepository.java new file mode 100644 index 0000000..08462a4 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppDirectChatRepository.java @@ -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 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 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> observeDirectChat(UUID chatId) { + return dao.get(chatId).observableResult() + .map(result -> new Optional<>(result.firstOrNull())) + .map(directChatMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Maybe getDirectChat(UUID chatId) { + return dao.get(chatId).maybe() + .map(directChatMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Single 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> 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 getDirectChatByPeer(Peer peer) { + return dao.getByPeer(peer.getId()).maybe() + .map(directChatMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeAllDirectChats() { + return dao.getAll().observableResult() + .map(ResultDelegate::toList) + .map(this::chatModelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + private List chatModelsToEntities(List models) { + List entities = new ArrayList<>(models.size()); + for (DirectChatModel model : models) { + entities.add(directChatMapping.toEntity(model)); + } + return entities; + } + + @Override + public Single 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 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()); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/XmppEntityCapsRepository.java b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppEntityCapsRepository.java new file mode 100644 index 0000000..ce9d4cf --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppEntityCapsRepository.java @@ -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 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> 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 mapModelsToEntities(Map models) { + Map entities = new ConcurrentHashMap<>(); + for (String key : models.keySet()) { + entities.put(key, entityCapsMapping.toEntity(models.get(key))); + } + return entities; + } + + private Map mapEntitiesToModels(Map entities) { + Map models = new ConcurrentHashMap<>(); + for (String key : entities.keySet()) { + models.put(key, entityCapsMapping.toModel(entities.get(key))); + } + return models; + } + + @Override + public Observable observeEntityCapsRecords() { + return data().select(EntityCapsModel.class) + .get().observableResult() + .flatMap(ReactiveResult::observable) + .map(entityCapsMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Maybe 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()); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/XmppGroupChatRepository.java b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppGroupChatRepository.java new file mode 100644 index 0000000..d43c316 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppGroupChatRepository.java @@ -0,0 +1,161 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.data.mapping.GroupChatMapping; +import org.mercury_im.messenger.data.model.GroupChatModel; +import org.mercury_im.messenger.data.repository.dao.GroupChatDao; +import org.mercury_im.messenger.util.Optional; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.chat.GroupChat; +import org.mercury_im.messenger.entity.chat.IGroupChat; +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; + +public class XmppGroupChatRepository + extends RequeryRepository + implements GroupChatRepository { + + private final GroupChatMapping groupChatMapping; + + private final GroupChatDao dao; + + @Inject + public XmppGroupChatRepository( + ReactiveEntityStore data, + @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, + @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler, + GroupChatMapping groupChatMapping) { + super(data, subscriberScheduler, observerScheduler); + this.groupChatMapping = groupChatMapping; + this.dao = new GroupChatDao(data); + } + + @Override + public Single insertGroupChat(GroupChat chat) { + return Single.just(chat) + .map(groupChatMapping::toModel) + .flatMap(dao::insert) + .map(model -> groupChatMapping.toEntity(model, chat)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeGroupChat(UUID chatId) { + return dao.get(chatId).observableResult() + .map(result -> new Optional<>(result.firstOrNull())) + .map(groupChatMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Maybe getGroupChat(UUID chatId) { + return dao.get(chatId).maybe() + .map(groupChatMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Single getOrCreateGroupChat(Account account, String roomAddress) { + return getGroupChatByRoomAddress(account, roomAddress) + .switchIfEmpty(Single.just(new IGroupChat()) + .map(chat -> { + chat.setAccount(account); + chat.setRoomAddress(roomAddress); + return chat; + }) + .flatMap(this::insertGroupChat)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeGroupChatByRoomAddress(UUID accountId, String roomAddress) { + return dao.get(accountId, roomAddress).observableResult() + .map(result -> new Optional<>(result.firstOrNull())) + .map(groupChatMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Maybe getGroupChatByRoomAddress(UUID accountId, String roomAddress) { + return dao.get(accountId, roomAddress).maybe() + .map(groupChatMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeAllGroupChats() { + return dao.getAll().observableResult() + .map(ResultDelegate::toList) + .map(this::modelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeAllGroupChatsOfAccount(UUID accountId) { + return data().select(GroupChatModel.class) + .where(GroupChatModel.ACCOUNT_ID.eq(accountId)) + .get().observableResult() + .map(ResultDelegate::toList) + .map(this::modelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + private List modelsToEntities(List models) { + List entities = new ArrayList<>(models.size()); + for (GroupChatModel model : models) { + entities.add(groupChatMapping.toEntity(model)); + } + return entities; + } + + @Override + public Single updateGroupChat(GroupChat chat) { + return dao.get(chat.getId()).maybe().toSingle() + .map(model -> groupChatMapping.toModel(chat, model)) + .flatMap(data()::update) + .map(model -> groupChatMapping.toEntity(model, chat)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Single upsertGroupChat(GroupChat chat) { + return dao.get(chat.getId()).maybe() + .switchIfEmpty(Single.just(chat).map(groupChatMapping::toModel) + .flatMap(dao::insert)) + .map(model -> groupChatMapping.toModel(chat, model)) + .flatMap(data()::update) + .map(model -> groupChatMapping.toEntity(model, chat)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Completable deleteGroupChat(UUID chatId) { + return dao.delete(chatId).ignoreElement() + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/XmppMessageRepository.java b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppMessageRepository.java new file mode 100644 index 0000000..9ef7259 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppMessageRepository.java @@ -0,0 +1,231 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.data.mapping.DirectChatMapping; +import org.mercury_im.messenger.data.mapping.GroupChatMapping; +import org.mercury_im.messenger.data.mapping.MessageMapping; +import org.mercury_im.messenger.data.model.DirectChatModel; +import org.mercury_im.messenger.data.model.DirectMessagesRelation; +import org.mercury_im.messenger.data.model.GroupChatModel; +import org.mercury_im.messenger.data.model.GroupMessagesRelation; +import org.mercury_im.messenger.data.model.MessageModel; +import org.mercury_im.messenger.data.model.MessagePayloadContainerModel; +import org.mercury_im.messenger.data.model.MessagePayloadModel; +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 org.mercury_im.messenger.entity.chat.DirectChat; +import org.mercury_im.messenger.entity.chat.GroupChat; +import org.mercury_im.messenger.entity.message.Message; +import org.mercury_im.messenger.util.ThreadUtils; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import io.reactivex.Completable; +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; + +public class XmppMessageRepository + extends RequeryRepository + implements MessageRepository { + + private final MessageMapping messageMapping; + private final DirectChatMapping directChatMapping; + private final GroupChatMapping groupChatMapping; + + private final DirectChatDao directChatDao; + private final GroupChatDao groupChatDao; + + private final MessageDao dao; + + + @Inject + public XmppMessageRepository(ReactiveEntityStore data, + @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, + @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler, + MessageMapping messageMapping, + DirectChatMapping directChatMapping, + GroupChatMapping groupChatMapping) { + super(data, subscriberScheduler, observerScheduler); + this.messageMapping = messageMapping; + this.directChatMapping = directChatMapping; + this.groupChatMapping = groupChatMapping; + this.directChatDao = new DirectChatDao(data); + this.groupChatDao = new GroupChatDao(data); + this.dao = new MessageDao(data); + } + + @Override + public Single insertMessage(DirectChat chat, Message message) { + return directChatDao.get(chat.getId()).maybe() + .switchIfEmpty(directChatDao.insert(directChatMapping.toModel(chat))) + .map(chatModel -> toRelation(chatModel, messageMapping.toModel(message))) + .flatMap(data()::insert) + .map(DirectMessagesRelation::getMessage) + .map(messageModel -> messageMapping.toEntity(messageModel, message)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Single insertMessage(GroupChat chat, Message message) { + return groupChatDao.get(chat.getId()).maybe() + .switchIfEmpty(groupChatDao.insert(groupChatMapping.toModel(chat))) + .map(chatModel -> toRelation(chatModel, messageMapping.toModel(message))) + .flatMap(data()::insert) + .map(GroupMessagesRelation::getMessage) + .map(messageModel -> messageMapping.toEntity(messageModel, message)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeMessages(DirectChat chat) { + return data().select(MessageModel.class) + .from(MessageModel.class) + .join(DirectMessagesRelation.class) + .on(DirectMessagesRelation.MESSAGE_ID.eq(MessageModel.ID)) + .get().observableResult() + .map(ResultDelegate::toList) + .map(this::messageModelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeMessages(GroupChat chat) { + return data().select(MessageModel.class) + .from(MessageModel.class) + .join(GroupMessagesRelation.class) + .on(GroupMessagesRelation.MESSAGE_ID.eq(MessageModel.ID)) + .where(GroupMessagesRelation.CHAT_ID.eq(chat.getId())) + .get().observableResult() + .map(ResultDelegate::toList) + .map(this::messageModelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> findMessagesWithBody(String body) { + return data().select(MessageModel.class) + .from(MessageModel.class) + .join(MessagePayloadContainerModel.class) + .on(MessagePayloadContainerModel.MESSAGE_ID.eq(MessageModel.ID)) + .join(MessagePayloadModel.class) + .on(MessagePayloadModel.PAYLOAD_CONTAINER_ID.eq(MessagePayloadContainerModel.ID)) + .where(MessagePayloadModel.BODY.eq(body)) + .get().observableResult() + .map(ResultDelegate::toList) + .map(this::messageModelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> findMessagesWithBody(DirectChat chat, String body) { + return data().select(MessageModel.class) + + .from(MessageModel.class) + .join(DirectMessagesRelation.class) + .on(DirectMessagesRelation.MESSAGE_ID.eq(MessageModel.ID)) + .join(MessagePayloadContainerModel.class) + .on(MessagePayloadContainerModel.MESSAGE_ID.eq(MessageModel.ID)) + .join(MessagePayloadModel.class) + .on(MessagePayloadModel.PAYLOAD_CONTAINER_ID.eq(MessagePayloadContainerModel.ID)) + + .where(MessagePayloadModel.BODY.eq(body)) + .and(DirectMessagesRelation.CHAT_ID.eq(chat.getId())) + .get().observableResult() + .map(ResultDelegate::toList) + .map(this::messageModelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> findMessagesWithBody(GroupChat chat, String body) { + return data().select(MessageModel.class) + + .from(MessageModel.class) + .join(GroupMessagesRelation.class) + .on(GroupMessagesRelation.MESSAGE_ID.eq(MessageModel.ID)) + .join(MessagePayloadContainerModel.class) + .on(MessagePayloadContainerModel.MESSAGE_ID.eq(MessageModel.ID)) + .join(MessagePayloadModel.class) + .on(MessagePayloadModel.PAYLOAD_CONTAINER_ID.eq(MessagePayloadContainerModel.ID)) + + .where(MessagePayloadModel.BODY.eq(body)) + .and(GroupMessagesRelation.CHAT_ID.eq(chat.getId())) + .get().observableResult() + .map(ResultDelegate::toList) + .map(this::messageModelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Single upsertMessage(DirectChat chat, Message message) { + return null; + /* + return data().select(DirectMessagesRelation.class) + .where(DirectMessagesRelation.CHAT_ID.eq(chat.getId())) + .and(DirectMessagesRelation.MESSAGE_ID.eq(message.getId())) + .get().maybe() + .switchIfEmpty(Single.just(message) + .map(messageMapping::toEntity) + .flatMap(messageModel -> ) + */ + } + + @Override + public Single upsertMessage(GroupChat chat, Message message) { + return null; + } + + @Override + public Single updateMessage(Message message) { + return dao.get(message.getId()).maybe().toSingle() + .map(model -> messageMapping.toModel(message, model)) + .flatMap(data()::update) + .map(model -> messageMapping.toEntity(model, message)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Completable deleteMessage(Message message) { + return data().delete(MessageModel.class) + .where(MessageModel.ID.eq(message.getId())) + .get().single().ignoreElement(); + } + + private DirectMessagesRelation toRelation(DirectChatModel chat, MessageModel message) { + DirectMessagesRelation relation = new DirectMessagesRelation(); + relation.setChat(chat); + relation.setMessage(message); + return relation; + } + + private GroupMessagesRelation toRelation(GroupChatModel chat, MessageModel message) { + GroupMessagesRelation relation = new GroupMessagesRelation(); + relation.setChat(chat); + relation.setMessage(message); + return relation; + } + + private List messageModelsToEntities(List models) { + List entities = new ArrayList<>(models.size()); + for (MessageModel model : models) { + entities.add(messageMapping.toEntity(model)); + } + return entities; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/XmppPeerRepository.java b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppPeerRepository.java new file mode 100644 index 0000000..b4194d4 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/XmppPeerRepository.java @@ -0,0 +1,208 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.data.mapping.PeerMapping; +import org.mercury_im.messenger.data.model.PeerModel; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.contact.IPeer; +import org.mercury_im.messenger.entity.contact.Peer; +import org.mercury_im.messenger.entity.contact.SubscriptionDirection; +import org.mercury_im.messenger.util.Optional; +import org.mercury_im.messenger.util.ThreadUtils; + +import java.util.ArrayList; +import java.util.Arrays; +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.Expression; +import io.requery.query.LogicalCondition; +import io.requery.query.ResultDelegate; +import io.requery.reactivex.ReactiveEntityStore; + +public class XmppPeerRepository + extends RequeryRepository + implements PeerRepository { + + private final AccountRepository accountRepository; + + private final PeerMapping peerMapping; + + @Inject + public XmppPeerRepository(ReactiveEntityStore data, + @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, + @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler, + PeerMapping peerMapping, AccountRepository accountRepository) { + super(data, subscriberScheduler, observerScheduler); + this.peerMapping = peerMapping; + this.accountRepository = accountRepository; + } + + @Override + public Single insertPeer(Peer peer) { + return data().insert(peerMapping.toModel(peer, new PeerModel())) + .map(model -> peerMapping.toEntity(model, peer)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observePeer(UUID peerId) { + return data().select(PeerModel.class) + .where(PeerModel.ID.eq(peerId)) + .get().observableResult() + .map(result -> new Optional<>(peerMapping.toEntity(result.firstOrNull(), new IPeer()))) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Maybe getPeer(UUID peerId) { + return data().select(PeerModel.class) + .where(PeerModel.ID.eq(peerId)) + .get().maybe() + .map(model -> peerMapping.toEntity(model, new IPeer())) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observePeerByAddress(UUID accountId, String address) { + return data().select(PeerModel.class) + .where(PeerModel.ACCOUNT_ID.eq(accountId)) + .and(PeerModel.ADDRESS.eq(address)) + .get().observableResult() + .map(result -> new Optional<>(result.firstOrNull())) + .map(peerMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Single getOrCreatePeer(UUID accountId, String address) { + return accountRepository.getAccount(accountId).toSingle() + .flatMap(account -> getOrCreatePeer(account, address)); + } + + @Override + public Maybe getPeerByAddress(UUID accountId, String address) { + return data().select(PeerModel.class) + .where(PeerModel.ACCOUNT_ID.eq(accountId)) + .and(PeerModel.ADDRESS.eq(address)) + .get().maybe() + .map(peerMapping::toEntity) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Single getOrCreatePeer(Account account, String address) { + return getPeerByAddress(account, address) + .switchIfEmpty(Single + .just(new IPeer()) + .map(peer -> { + peer.setAccount(account); + peer.setAddress(address); + return peer; + }) + .flatMap(this::insertPeer)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Observable> observeAllPeers() { + return data().select(PeerModel.class) + .orderBy(PeerModel.ADDRESS) + .get().observableResult() + .map(ResultDelegate::toList) + .map(this::peerModelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + private List peerModelsToEntities(List peerModels) { + List peerEntities = new ArrayList<>(peerModels.size()); + for (PeerModel model : peerModels) { + peerEntities.add(peerMapping.toEntity(model)); + } + return peerEntities; + } + + @Override + public Observable> observeAllContactsOfAccount(UUID accountId) { + return data().select(PeerModel.class) + .where(PeerModel.ACCOUNT_ID.eq(accountId)) + .and(isContact()) + .get().observableResult() + .map(ResultDelegate::toList) + .map(this::peerModelsToEntities) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + private LogicalCondition, ?> isContact() { + return PeerModel.SUBSCRIPTION_DIRECTION.in(Arrays.asList( + SubscriptionDirection.both, + SubscriptionDirection.to)); + } + + @Override + public Single updatePeer(Peer peer) { + // In order to update, we fetch the model, update it and write it back. + return data().select(PeerModel.class) + .where(PeerModel.ID.eq(peer.getId())) + .get().maybe().toSingle() + // write changes into model + .map(model -> peerMapping.toModel(peer, model)) + .flatMap(data()::update) + .map(model -> peerMapping.toEntity(model, peer)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Single upsertPeer(Peer peer) { + return data().select(PeerModel.class) + .where(PeerModel.ID.eq(peer.getId())) + .get().maybe() + // if not exists, create + .switchIfEmpty(Single.just(peer) + .map(peerMapping::toModel) + .flatMap(data()::insert)) + // write changes into fetched model + .map(model -> peerMapping.toModel(peer, model)) + // write changed model back to db + .flatMap(data()::update) + .map(model -> peerMapping.toEntity(model, peer)) + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Completable deletePeer(Peer peer) { + return data().delete(PeerModel.class) + .where(PeerModel.ID.eq(peer.getId())) + .get().single().ignoreElement() + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } + + @Override + public Completable deletePeer(UUID accountId, String address) { + return data().delete(PeerModel.class) + .where(PeerModel.ACCOUNT_ID.eq(accountId) + .and(PeerModel.ADDRESS.eq(address))) + .get().single().ignoreElement() + .subscribeOn(subscriberScheduler()) + .observeOn(observerScheduler()); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/dao/AccountDao.java b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/AccountDao.java new file mode 100644 index 0000000..3aaf9d4 --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/AccountDao.java @@ -0,0 +1,47 @@ +package org.mercury_im.messenger.data.repository.dao; + +import org.mercury_im.messenger.data.model.AccountModel; + +import java.util.UUID; + +import javax.inject.Inject; + +import io.reactivex.Single; +import io.requery.Persistable; +import io.requery.reactivex.ReactiveEntityStore; +import io.requery.reactivex.ReactiveResult; + +public class AccountDao extends RequeryDao { + + @Inject + public AccountDao(ReactiveEntityStore data) { + super(data); + } + + public Single insert(AccountModel account) { + return data().insert(account); + } + + public ReactiveResult get(UUID accountId) { + return data().select(AccountModel.class) + .where(AccountModel.ID.eq(accountId)) + .get(); + } + + public ReactiveResult get(String address) { + return data().select(AccountModel.class) + .where(AccountModel.ADDRESS.eq(address)) + .get(); + } + + public ReactiveResult getAll() { + return data().select(AccountModel.class) + .get(); + } + + public Single delete(UUID accountId) { + return data().delete(AccountModel.class) + .where(AccountModel.ID.eq(accountId)) + .get().single(); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/dao/DirectChatDao.java b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/DirectChatDao.java new file mode 100644 index 0000000..6f7f68b --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/DirectChatDao.java @@ -0,0 +1,48 @@ +package org.mercury_im.messenger.data.repository.dao; + +import org.mercury_im.messenger.data.model.DirectChatModel; + +import java.util.UUID; + +import javax.inject.Inject; + +import io.reactivex.Single; +import io.requery.Persistable; +import io.requery.reactivex.ReactiveEntityStore; +import io.requery.reactivex.ReactiveResult; + +public class DirectChatDao extends RequeryDao { + + @Inject + public DirectChatDao(ReactiveEntityStore data) { + super(data); + } + + public Single insert(DirectChatModel chat) { + return data().insert(chat); + } + + public ReactiveResult get(UUID chatId) { + return data().select(DirectChatModel.class) + .where(DirectChatModel.ID.eq(chatId)) + .get(); + } + + public ReactiveResult getByPeer(UUID peerId) { + return data().select(DirectChatModel.class) + .where(DirectChatModel.PEER_ID.eq(peerId)) + .get(); + } + + public ReactiveResult getAll() { + return data().select(DirectChatModel.class) + .get(); + } + + public Single delete(UUID chatId) { + return data().delete(DirectChatModel.class) + .where(DirectChatModel.ID.eq(chatId)) + .get() + .single(); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/dao/GroupChatDao.java b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/GroupChatDao.java new file mode 100644 index 0000000..9997ceb --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/GroupChatDao.java @@ -0,0 +1,49 @@ +package org.mercury_im.messenger.data.repository.dao; + +import org.mercury_im.messenger.data.model.GroupChatModel; + +import java.util.UUID; + +import javax.inject.Inject; + +import io.reactivex.Single; +import io.requery.Persistable; +import io.requery.reactivex.ReactiveEntityStore; +import io.requery.reactivex.ReactiveResult; + +public class GroupChatDao extends RequeryDao { + + @Inject + public GroupChatDao(ReactiveEntityStore data) { + super(data); + } + + public Single insert(GroupChatModel chat) { + return data().insert(chat); + } + + public ReactiveResult get(UUID chatId) { + return data().select(GroupChatModel.class) + .where(GroupChatModel.ID.eq(chatId)) + .get(); + } + + public ReactiveResult get(UUID accountId, String address) { + return data().select(GroupChatModel.class) + .where(GroupChatModel.ACCOUNT_ID.eq(accountId)) + .and(GroupChatModel.ADDRESS.eq(address)) + .get(); + } + + public ReactiveResult getAll() { + return data().select(GroupChatModel.class) + .get(); + } + + public Single delete(UUID chatId) { + return data().delete(GroupChatModel.class) + .where(GroupChatModel.ID.eq(chatId)) + .get() + .single(); + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/dao/MessageDao.java b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/MessageDao.java new file mode 100644 index 0000000..c9c6a5f --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/MessageDao.java @@ -0,0 +1,52 @@ +package org.mercury_im.messenger.data.repository.dao; + +import org.mercury_im.messenger.data.model.DirectChatModel; +import org.mercury_im.messenger.data.model.DirectMessagesRelation; +import org.mercury_im.messenger.data.model.GroupChatModel; +import org.mercury_im.messenger.data.model.GroupMessagesRelation; +import org.mercury_im.messenger.data.model.MessageModel; + +import java.util.UUID; + +import io.reactivex.Single; +import io.requery.Persistable; +import io.requery.reactivex.ReactiveEntityStore; +import io.requery.reactivex.ReactiveResult; + +public class MessageDao extends RequeryDao { + + private final DirectChatDao directChatDao; + private final GroupChatDao groupChatDao; + + public MessageDao(ReactiveEntityStore data) { + super(data); + this.directChatDao = new DirectChatDao(data); + this.groupChatDao = new GroupChatDao(data); + } + + public Single insert(MessageModel message) { + return data().insert(message); + } + + public ReactiveResult get(UUID messageId) { + return data().select(MessageModel.class) + .where(MessageModel.ID.eq(messageId)) + .get(); + } + + + + private DirectMessagesRelation toRelation(DirectChatModel chat, MessageModel message) { + DirectMessagesRelation relation = new DirectMessagesRelation(); + relation.setChat(chat); + relation.setMessage(message); + return relation; + } + + private GroupMessagesRelation toRelation(GroupChatModel chat, MessageModel message) { + GroupMessagesRelation relation = new GroupMessagesRelation(); + relation.setChat(chat); + relation.setMessage(message); + return relation; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/dao/RequeryDao.java b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/RequeryDao.java new file mode 100644 index 0000000..813ef3b --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/dao/RequeryDao.java @@ -0,0 +1,17 @@ +package org.mercury_im.messenger.data.repository.dao; + +import io.requery.Persistable; +import io.requery.reactivex.ReactiveEntityStore; + +public abstract class RequeryDao { + + private final ReactiveEntityStore data; + + public RequeryDao(ReactiveEntityStore data) { + this.data = data; + } + + public ReactiveEntityStore data() { + return data; + } +} diff --git a/data/src/main/java/org/mercury_im/messenger/data/repository/package-info.java b/data/src/main/java/org/mercury_im/messenger/data/repository/package-info.java new file mode 100644 index 0000000..3dfd22e --- /dev/null +++ b/data/src/main/java/org/mercury_im/messenger/data/repository/package-info.java @@ -0,0 +1,11 @@ +/** + * The repository package contains implementations of the repositories defined in the domain + * module. + * + * While the data module uses requery to store data in an SQL database, the repositories use + * mappers defined in the mapping package to map the requery models to entities. + * + * Since the application itself only ever uses entities, it doesn't have to know about the database + * at all. + */ +package org.mercury_im.messenger.data.repository; diff --git a/data/src/test/java/org/mercury_im/messenger/data/di/InMemoryDatabaseComponent.java b/data/src/test/java/org/mercury_im/messenger/data/di/InMemoryDatabaseComponent.java new file mode 100644 index 0000000..37e71d6 --- /dev/null +++ b/data/src/test/java/org/mercury_im/messenger/data/di/InMemoryDatabaseComponent.java @@ -0,0 +1,18 @@ +package org.mercury_im.messenger.data.di; + +import org.mercury_im.messenger.data.repository.AccountRepositoryTest; + +import javax.inject.Singleton; + +import dagger.Component; + +@Component(modules = { + RepositoryModule.class, + TestDatabaseModule.class, + TestingSchedulerModule.class +}) +@Singleton +public interface InMemoryDatabaseComponent { + + void inject(AccountRepositoryTest test); +} diff --git a/data/src/test/java/org/mercury_im/messenger/data/di/MappingTestComponent.java b/data/src/test/java/org/mercury_im/messenger/data/di/MappingTestComponent.java new file mode 100644 index 0000000..6c8ff56 --- /dev/null +++ b/data/src/test/java/org/mercury_im/messenger/data/di/MappingTestComponent.java @@ -0,0 +1,21 @@ +package org.mercury_im.messenger.data.di; + + +import org.mercury_im.messenger.data.mapping.AccountMappingTest; +import org.mercury_im.messenger.data.mapping.EntityCapsMappingTest; +import org.mercury_im.messenger.data.mapping.PeerMappingTest; + +import javax.inject.Singleton; + +import dagger.Component; + +@Component(modules = MappingModule.class) +@Singleton +public interface MappingTestComponent { + + void inject(AccountMappingTest test); + + void inject(PeerMappingTest test); + + void inject(EntityCapsMappingTest test); +} diff --git a/data/src/test/java/org/mercury_im/messenger/data/di/TestDatabaseModule.java b/data/src/test/java/org/mercury_im/messenger/data/di/TestDatabaseModule.java new file mode 100644 index 0000000..e16e621 --- /dev/null +++ b/data/src/test/java/org/mercury_im/messenger/data/di/TestDatabaseModule.java @@ -0,0 +1,59 @@ +package org.mercury_im.messenger.data.di; + +import org.mercury_im.messenger.data.model.Models; +import org.sqlite.SQLiteConfig; +import org.sqlite.SQLiteDataSource; + +import java.io.PrintWriter; +import java.sql.SQLException; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import io.requery.Persistable; +import io.requery.cache.WeakEntityCache; +import io.requery.meta.EntityModel; +import io.requery.reactivex.ReactiveEntityStore; +import io.requery.reactivex.ReactiveSupport; +import io.requery.sql.Configuration; +import io.requery.sql.ConfigurationBuilder; +import io.requery.sql.EntityDataStore; +import io.requery.sql.SchemaModifier; +import io.requery.sql.TableCreationMode; + +@Module +public class TestDatabaseModule { + + @Provides + @Singleton + public static ReactiveEntityStore provideInMemoryEntityStore() { + EntityModel model = Models.DEFAULT; + + SQLiteDataSource dataSource = new SQLiteDataSource(); + try { + dataSource.setLogWriter(new PrintWriter(System.out)); + } catch (SQLException e) { + e.printStackTrace(); + } + dataSource.setUrl("jdbc:sqlite:/tmp/mercury_testing.db"); + dataSource.setDatabaseName("testing"); + SQLiteConfig config = new SQLiteConfig(); + config.setDateClass("TEXT"); + dataSource.setConfig(config); + // Turn on foreign keys support. + // NOTE: Do it after setConfig, or setConfig will overwrite this setting + dataSource.setEnforceForeignKeys(true); + + SchemaModifier modifier = new SchemaModifier(dataSource, model); + modifier.createTables(TableCreationMode.DROP_CREATE); + + Configuration configuration = new ConfigurationBuilder(dataSource, model) + .setEntityCache(new WeakEntityCache()) + .useDefaultLogging() + .build(); + + EntityDataStore dataStore = new EntityDataStore<>(configuration); + return ReactiveSupport.toReactiveStore(dataStore); + } +} diff --git a/data/src/test/java/org/mercury_im/messenger/data/di/TestingSchedulerModule.java b/data/src/test/java/org/mercury_im/messenger/data/di/TestingSchedulerModule.java new file mode 100644 index 0000000..4216a43 --- /dev/null +++ b/data/src/test/java/org/mercury_im/messenger/data/di/TestingSchedulerModule.java @@ -0,0 +1,29 @@ +package org.mercury_im.messenger.data.di; + +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.reactivex.schedulers.Schedulers; + +@Module +public class TestingSchedulerModule { + + @Provides + @Singleton + @Named(value = ThreadUtils.SCHEDULER_IO) + public static Scheduler provideSubscriberScheduler() { + return Schedulers.io(); + } + + @Provides + @Singleton + @Named(value = ThreadUtils.SCHEDULER_UI) + public static Scheduler provideObserverScheduler() { + return Schedulers.trampoline(); + } +} diff --git a/data/src/test/java/org/mercury_im/messenger/data/mapping/AccountMappingTest.java b/data/src/test/java/org/mercury_im/messenger/data/mapping/AccountMappingTest.java new file mode 100644 index 0000000..b2bdb99 --- /dev/null +++ b/data/src/test/java/org/mercury_im/messenger/data/mapping/AccountMappingTest.java @@ -0,0 +1,67 @@ +package org.mercury_im.messenger.data.mapping; + +import org.junit.Test; +import org.mercury_im.messenger.data.di.DaggerMappingTestComponent; +import org.mercury_im.messenger.data.model.AccountModel; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.IAccount; + +import java.lang.reflect.Field; + +import javax.inject.Inject; + +import io.requery.proxy.EntityProxy; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotSame; + +public class AccountMappingTest { + + @Inject + AccountMapping accountMapping; + + public static final Account ACCOUNT_MISSION_CONTROL; + public static final Account ACCOUNT_LITTLE_JOE; + + static { + ACCOUNT_MISSION_CONTROL = new IAccount(); + ACCOUNT_MISSION_CONTROL.setAddress("mission-controll@planet.earth"); + ACCOUNT_MISSION_CONTROL.setEnabled(true); + ACCOUNT_MISSION_CONTROL.setPassword("notBecauseTheyAreEasy"); + + ACCOUNT_LITTLE_JOE = new IAccount(); + ACCOUNT_LITTLE_JOE.setAddress("little-joe@planet.earth"); + ACCOUNT_LITTLE_JOE.setEnabled(false); + ACCOUNT_LITTLE_JOE.setPassword("butBecauseTheyAreHard"); + } + + public AccountMappingTest() { + DaggerMappingTestComponent.create().inject(this); + } + + @Test + public void entityToModel() { + + AccountModel model = accountMapping.toModel(ACCOUNT_MISSION_CONTROL); + + assertEquals(ACCOUNT_MISSION_CONTROL.getId(), model.getId()); + assertEquals(ACCOUNT_MISSION_CONTROL.getAddress(), model.getAddress()); + assertEquals(ACCOUNT_MISSION_CONTROL.getPassword(), model.getPassword()); + assertEquals(ACCOUNT_MISSION_CONTROL.isEnabled(), model.isEnabled()); + } + + @Test + public void modelToEntity() throws NoSuchFieldException, IllegalAccessException { + AccountModel model = new AccountModel(); + model.getId(); + model.setAddress("model@entity.store"); + model.setEnabled(true); + model.setPassword("12345"); + + Account entity = accountMapping.toEntity(model); + + assertEquals(model.getId(), entity.getId()); + assertEquals(model.getAddress(), entity.getAddress()); + assertEquals(model.getPassword(), entity.getPassword()); + } +} diff --git a/data/src/test/java/org/mercury_im/messenger/data/mapping/EntityCapsMappingTest.java b/data/src/test/java/org/mercury_im/messenger/data/mapping/EntityCapsMappingTest.java new file mode 100644 index 0000000..f684810 --- /dev/null +++ b/data/src/test/java/org/mercury_im/messenger/data/mapping/EntityCapsMappingTest.java @@ -0,0 +1,74 @@ +package org.mercury_im.messenger.data.mapping; + +import org.junit.Test; +import org.mercury_im.messenger.data.di.DaggerMappingTestComponent; +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 static junit.framework.TestCase.assertEquals; + +public class EntityCapsMappingTest { + + @Inject + EntityCapsMapping mapping; + + public EntityCapsMappingTest() { + DaggerMappingTestComponent.create().inject(this); + } + + @Test + public void mapEntityToModelTest() { + EntityCapsRecord entity = new IEntityCapsRecord(); + entity.setNodeVer("thisisahash"); + entity.setXml(""); + + EntityCapsModel model = mapping.toModel(entity); + + assertEquals(entity.getNodeVer(), model.getNodeVer()); + assertEquals(entity.getXml(), model.getXml()); + } + + @Test + public void mapModelToEntityTest() { + EntityCapsModel model = new EntityCapsModel(); + model.setNodeVer("q07IKJEyjvHSyhy//CH0CxmKi8w="); + model.setXml("" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " urn:xmpp:dataforms:softwareinfo" + + " " + + " " + + " ipv4" + + " ipv6" + + " " + + " " + + " Mac" + + " " + + " " + + " 10.5.1" + + " " + + " " + + " Psi" + + " " + + " " + + " 0.11" + + " " + + " " + + " "); + + EntityCapsRecord entity = mapping.toEntity(model); + + assertEquals(model.getNodeVer(), entity.getNodeVer()); + assertEquals(model.getXml(), entity.getXml()); + } +} diff --git a/data/src/test/java/org/mercury_im/messenger/data/mapping/PeerMappingTest.java b/data/src/test/java/org/mercury_im/messenger/data/mapping/PeerMappingTest.java new file mode 100644 index 0000000..84745ca --- /dev/null +++ b/data/src/test/java/org/mercury_im/messenger/data/mapping/PeerMappingTest.java @@ -0,0 +1,49 @@ +package org.mercury_im.messenger.data.mapping; + +import org.junit.Test; +import org.mercury_im.messenger.data.di.DaggerMappingTestComponent; +import org.mercury_im.messenger.data.model.AccountModel; +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 org.mercury_im.messenger.entity.contact.SubscriptionDirection; + +import javax.inject.Inject; + +import static junit.framework.TestCase.assertEquals; + +public class PeerMappingTest { + + @Inject + PeerMapping peerMapping; + + public static final Peer PEER_GORDO; + + static { + PEER_GORDO = new IPeer(); + PEER_GORDO.setAccount(AccountMappingTest.ACCOUNT_MISSION_CONTROL); + PEER_GORDO.setAddress("gordo@big.joe"); + PEER_GORDO.setName("Gordo"); + PEER_GORDO.setSubscriptionDirection(SubscriptionDirection.both); + } + + public PeerMappingTest() { + DaggerMappingTestComponent.create().inject(this); + } + + @Test + public void entityToModel() { + PeerModel model = peerMapping.toModel(PEER_GORDO); + assertEquals(PEER_GORDO.getAddress(), model.getAddress()); + assertEquals(PEER_GORDO.getAccount().getAddress(), model.getAccount().getAddress()); + assertEquals(PEER_GORDO.getName(), model.getName()); + } + + @Test + public void modelToEntity() { + PeerModel model = new PeerModel(); + model.setName("Gordo"); + model.setAddress("gordo@big.joe"); + model.setAccount(new AccountModel()); + } +} diff --git a/data/src/test/java/org/mercury_im/messenger/data/repository/AccountRepositoryTest.java b/data/src/test/java/org/mercury_im/messenger/data/repository/AccountRepositoryTest.java new file mode 100644 index 0000000..0aba3d7 --- /dev/null +++ b/data/src/test/java/org/mercury_im/messenger/data/repository/AccountRepositoryTest.java @@ -0,0 +1,131 @@ +package org.mercury_im.messenger.data.repository; + +import org.junit.Test; +import org.mercury_im.messenger.data.di.DaggerInMemoryDatabaseComponent; +import org.mercury_im.messenger.data.di.InMemoryDatabaseComponent; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.IAccount; + +import java.util.NoSuchElementException; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import io.reactivex.disposables.CompositeDisposable; +import io.requery.Persistable; +import io.requery.reactivex.ReactiveEntityStore; + +public class AccountRepositoryTest { + + private static final Logger LOGGER = Logger.getLogger(AccountRepositoryTest.class.getName()); + + @Inject + ReactiveEntityStore dataStore; + + @Inject + XmppAccountRepository accountRepository; + + @Inject + XmppDirectChatRepository directChatRepository; + + @Inject + XmppPeerRepository contactRepository; + + @Inject + XmppMessageRepository messageRepository; + + @Inject + public AccountRepositoryTest() { + InMemoryDatabaseComponent testComponent = DaggerInMemoryDatabaseComponent.builder() + .build(); + testComponent.inject(this); + } + + @Test + public void test() throws InterruptedException { + CompositeDisposable d = new CompositeDisposable(); + d.add(accountRepository.observeAccounts() + .distinct(Account::getId) + .subscribe(a -> System.out.println("Observe: " + a.getAddress()))); + + Thread.sleep(100); + + Account a1 = new IAccount(); + a1.setAddress("a1@example.com"); + a1.setPassword("a1a1a1"); + a1.setEnabled(true); + + d.add(accountRepository.insertAccount(a1).subscribe()); + + Thread.sleep(100); + + Account a2 = new IAccount(); + a2.setAddress("a2@example.com"); + a2.setPassword("a2a2a2"); + a2.setEnabled(false); + + d.add(accountRepository.insertAccount(a2).subscribe()); + + Thread.sleep(100); + + Account a3 = new IAccount(); + a3.setAddress("a3@example.com"); + a3.setPassword("a3a3a3"); + a3.setEnabled(false); + + d.add(accountRepository.insertAccount(a3).subscribe()); + + Thread.sleep(100); + + a1.setAddress("a11@example.org"); + + d.add(accountRepository.updateAccount(a1).subscribe()); + + Thread.sleep(100); + } + + @Test + public void observeDeletionTest() throws InterruptedException { + CompositeDisposable d = new CompositeDisposable(); + + UUID uuid = UUID.randomUUID(); + d.add(accountRepository.observeAccount(uuid) + .subscribe(optAccount -> { + LOGGER.log(Level.INFO, "Changed"); + if (!optAccount.isPresent()) { + LOGGER.log(Level.INFO, "Changed: Account " + uuid.toString() + " is not present."); + } else { + LOGGER.log(Level.INFO, "Changed: Account " + uuid.toString() + ": " + optAccount.getItem().toString()); + } + })); + + Account account = new IAccount(uuid); + account.setEnabled(true); + account.setAddress("hello@world"); + account.setPassword("wooooooh"); + + d.add(accountRepository.insertAccount(account) + .subscribe(insert -> LOGGER.log(Level.INFO, "Inserted."), + error -> LOGGER.log(Level.SEVERE, "Inserted", error))); + + Thread.sleep(100); + + d.add(accountRepository.deleteAccount(uuid) + .subscribe(() -> LOGGER.log(Level.INFO, "Deleted"))); + + Thread.sleep(1000); + d.dispose(); + } + + @Test(expected = NoSuchElementException.class) + public void updateMissingEntityFails() { + Account missingAccount = new IAccount(); + missingAccount.setAddress("this@account.is.missing"); + missingAccount.setPassword("inTheDatabase"); + + accountRepository.updateAccount(missingAccount) + .blockingGet(); + } +} diff --git a/domain/.gitignore b/domain/.gitignore new file mode 100644 index 0000000..60bf954 --- /dev/null +++ b/domain/.gitignore @@ -0,0 +1,2 @@ +/build +testcredentials.properties diff --git a/domain/build.gradle b/domain/build.gradle new file mode 100644 index 0000000..187c55a --- /dev/null +++ b/domain/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'java-library' + +dependencies { + + implementation project(':entity') + + // Smack + // Not all of those are needed, but it may be a good idea to define those versions explicitly + api "org.igniterealtime.smack:smack-core:$smackCoreVersion" + api "org.igniterealtime.smack:smack-experimental:$smackExperimentalVersion" + api "org.igniterealtime.smack:smack-extensions:$smackExtensionsVersion" + api "org.igniterealtime.smack:smack-im:$smackImVersion" + api "org.igniterealtime.smack:smack-tcp:$smackTcpVersion" + + testImplementation "org.igniterealtime.smack:smack-java7:$smackJava7Version" + + // RxJava2 + implementation "io.reactivex.rxjava2:rxjava:$rxJava2Version" + + // 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' + + // JUnit for testing + testImplementation "junit:junit:$junitVersion" +} + +sourceCompatibility = "8" +targetCompatibility = "8" diff --git a/domain/src/main/java/org/mercury_im/messenger/ClientStateListener.java b/domain/src/main/java/org/mercury_im/messenger/ClientStateListener.java new file mode 100644 index 0000000..61964d0 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/ClientStateListener.java @@ -0,0 +1,8 @@ +package org.mercury_im.messenger; + +public interface ClientStateListener { + + void onClientInForeground(); + + void onClientInBackground(); +} diff --git a/domain/src/main/java/org/mercury_im/messenger/MercurySchedulers.java b/domain/src/main/java/org/mercury_im/messenger/MercurySchedulers.java new file mode 100644 index 0000000..a51d9c1 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/MercurySchedulers.java @@ -0,0 +1,24 @@ +package org.mercury_im.messenger; + +import org.mercury_im.messenger.util.ThreadUtils; + +import javax.inject.Inject; +import javax.inject.Named; + +import io.reactivex.Scheduler; +import lombok.Getter; + +public class MercurySchedulers { + + @Getter + private final Scheduler subscriberScheduler; + @Getter + private final Scheduler observerScheduler; + + @Inject + public MercurySchedulers(@Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, + @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler) { + this.subscriberScheduler = subscriberScheduler; + this.observerScheduler = observerScheduler; + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/MessageCenter.java b/domain/src/main/java/org/mercury_im/messenger/MessageCenter.java new file mode 100644 index 0000000..995f780 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/MessageCenter.java @@ -0,0 +1,16 @@ +package org.mercury_im.messenger; + +import org.mercury_im.messenger.listener.IncomingDirectMessageListener; +import org.mercury_im.messenger.entity.chat.Chat; +import org.mercury_im.messenger.entity.message.Message; + +import io.reactivex.Completable; + +public interface MessageCenter { + + Messenger getMessenger(); + + Completable sendMessage(Message message, C chat); + + void addIncomingMessageListener(IncomingDirectMessageListener listener); +} diff --git a/domain/src/main/java/org/mercury_im/messenger/Messenger.java b/domain/src/main/java/org/mercury_im/messenger/Messenger.java new file mode 100644 index 0000000..1e53829 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/Messenger.java @@ -0,0 +1,86 @@ +package org.mercury_im.messenger; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smackx.csi.ClientStateIndicationManager; +import org.mercury_im.messenger.data.repository.Repositories; +import org.mercury_im.messenger.usecase.AddAccount; +import org.mercury_im.messenger.usecase.RosterStoreBinder; +import org.mercury_im.messenger.xmpp.MercuryConnection; +import org.mercury_im.messenger.xmpp.MercuryConnectionManager; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; + +@Singleton +public class Messenger implements ClientStateListener { + + public static final String TAG = "MercuryIM"; + private static final Logger LOGGER = Logger.getLogger(Messenger.class.getName()); + + private final MercuryConnectionManager connectionManager; + private final Repositories repositories; + + private CompositeDisposable disposable = new CompositeDisposable(); + + @Inject + public Messenger(Repositories repositories, MercuryConnectionManager connectionManager) { + this.repositories = repositories; + this.connectionManager = connectionManager; + performInitialLogin(); + } + + public MercuryConnectionManager getConnectionManager() { + return connectionManager; + } + + public void performInitialLogin() { + LOGGER.log(Level.INFO, "Perform initial login."); + disposable.add(repositories.getAccountRepository().observeAllAccounts().firstOrError() + .subscribeOn(Schedulers.newThread()) + .subscribe(connectionManager::registerConnections)); + } + + public AddAccount addAccount() { + return new AddAccount(repositories.getAccountRepository(), this.connectionManager); + } + + // CSI + + @Override + public void onClientInForeground() { + LOGGER.log(Level.INFO, "CSI: active"); + for (MercuryConnection connection : connectionManager.getConnections()) { + tryCsiActive(connection); + } + } + + @Override + public void onClientInBackground() { + LOGGER.log(Level.INFO, "CSI: inactive"); + for (MercuryConnection connection : connectionManager.getConnections()) { + tryCsiInactive(connection); + } + } + + private void tryCsiActive(MercuryConnection connection) { + try { + ClientStateIndicationManager.active(connection.getConnection()); + } catch (SmackException.NotConnectedException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Sending CSI state 'active' failed.", e); + } + } + + private void tryCsiInactive(MercuryConnection connection) { + try { + ClientStateIndicationManager.inactive(connection.getConnection()); + } catch (SmackException.NotConnectedException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Sending CSI state 'inactive' failed.", e); + } + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/account/error/PasswordError.java b/domain/src/main/java/org/mercury_im/messenger/account/error/PasswordError.java new file mode 100644 index 0000000..6efc818 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/account/error/PasswordError.java @@ -0,0 +1,8 @@ +package org.mercury_im.messenger.account.error; + +public enum PasswordError { + none, + emptyPassword, + invalidPassword, + incorrectPassword +} \ No newline at end of file diff --git a/domain/src/main/java/org/mercury_im/messenger/account/error/UsernameError.java b/domain/src/main/java/org/mercury_im/messenger/account/error/UsernameError.java new file mode 100644 index 0000000..33b9030 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/account/error/UsernameError.java @@ -0,0 +1,8 @@ +package org.mercury_im.messenger.account.error; + +public enum UsernameError { + none, + emptyUsername, + invalidUsername, + unknownUsername +} diff --git a/domain/src/main/java/org/mercury_im/messenger/data/repository/AccountRepository.java b/domain/src/main/java/org/mercury_im/messenger/data/repository/AccountRepository.java new file mode 100644 index 0000000..18eae4a --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/data/repository/AccountRepository.java @@ -0,0 +1,48 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.util.Optional; + +import java.util.List; +import java.util.UUID; + +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; + +public interface AccountRepository { + + Single insertAccount(Account account); + + default Observable> observeAccount(Account account) { + return observeAccount(account.getId()); + } + + Observable> observeAccount(UUID accountId); + + default Maybe getAccount(Account account) { + return getAccount(account.getId()); + } + + Maybe getAccount(UUID accountId); + + Observable> observeAccountByAddress(String address); + + Maybe getAccountByAddress(String address); + + Observable> observeAllAccounts(); + + Observable observeAccounts(); + + Single updateAccount(Account account); + + Single upsertAccount(Account account); + + default Completable deleteAccount(Account account) { + return deleteAccount(account.getId()); + } + + Completable deleteAccount(UUID accountId); + +} diff --git a/domain/src/main/java/org/mercury_im/messenger/data/repository/DirectChatRepository.java b/domain/src/main/java/org/mercury_im/messenger/data/repository/DirectChatRepository.java new file mode 100644 index 0000000..e15c442 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/data/repository/DirectChatRepository.java @@ -0,0 +1,45 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.util.Optional; +import org.mercury_im.messenger.entity.chat.DirectChat; +import org.mercury_im.messenger.entity.contact.Peer; + +import java.util.List; +import java.util.UUID; + +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; + +public interface DirectChatRepository { + + Single insertDirectChat(DirectChat chat); + + default Observable> observeDirectChat(DirectChat chat) { + return observeDirectChat(chat.getId()); + } + + Observable> observeDirectChat(UUID chatId); + + Maybe getDirectChat(UUID chatId); + + Single getOrCreateChatWithPeer(Peer peer); + + Observable> observeDirectChatByPeer(Peer peer); + + Maybe getDirectChatByPeer(Peer peer); + + Observable> observeAllDirectChats(); + + Single updateDirectChat(DirectChat chat); + + Single upsertDirectChat(DirectChat chat); + + default Completable deleteDirectChat(DirectChat chat) { + return deleteDirectChat(chat.getId()); + } + + Completable deleteDirectChat(UUID chatId); + +} diff --git a/domain/src/main/java/org/mercury_im/messenger/data/repository/EntityCapsRepository.java b/domain/src/main/java/org/mercury_im/messenger/data/repository/EntityCapsRepository.java new file mode 100644 index 0000000..6273464 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/data/repository/EntityCapsRepository.java @@ -0,0 +1,20 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.entity.caps.EntityCapsRecord; + +import java.util.Map; + +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Observable; + +public interface EntityCapsRepository { + + Observable> observeAllEntityCapsRecords(); + + Observable observeEntityCapsRecords(); + + Maybe maybeGetEntityCapsRecord(String nodeVer); + + Completable insertEntityCapsRecord(EntityCapsRecord entityCapsRecord); +} diff --git a/domain/src/main/java/org/mercury_im/messenger/data/repository/GroupChatRepository.java b/domain/src/main/java/org/mercury_im/messenger/data/repository/GroupChatRepository.java new file mode 100644 index 0000000..0c79ef2 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/data/repository/GroupChatRepository.java @@ -0,0 +1,55 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.chat.GroupChat; +import org.mercury_im.messenger.util.Optional; + +import java.util.List; +import java.util.UUID; + +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; + +public interface GroupChatRepository { + + Single insertGroupChat(GroupChat chat); + + Observable> observeGroupChat(UUID chatId); + + Maybe getGroupChat(UUID chatId); + + Single getOrCreateGroupChat(Account account, String roomAddress); + + default Observable> observeGroupChatByRoomAddress(Account account, String roomAddress) { + return observeGroupChatByRoomAddress(account.getId(), roomAddress); + } + + Observable> observeGroupChatByRoomAddress(UUID accountId, String roomAddress); + + default Maybe getGroupChatByRoomAddress(Account account, String roomAddress) { + return getGroupChatByRoomAddress(account.getId(), roomAddress); + } + + Maybe getGroupChatByRoomAddress(UUID accountId, String roomAddress); + + Observable> observeAllGroupChats(); + + default Observable> observeAllGroupChatsOfAccount(Account account) { + return observeAllGroupChatsOfAccount(account.getId()); + } + + Observable> observeAllGroupChatsOfAccount(UUID accountId); + + Single updateGroupChat(GroupChat chat); + + Single upsertGroupChat(GroupChat chat); + + default Completable deleteGroupChat(GroupChat chat) { + return deleteGroupChat(chat.getId()); + } + + Completable deleteGroupChat(UUID chatId); + +} diff --git a/domain/src/main/java/org/mercury_im/messenger/data/repository/MessageRepository.java b/domain/src/main/java/org/mercury_im/messenger/data/repository/MessageRepository.java new file mode 100644 index 0000000..bed2cea --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/data/repository/MessageRepository.java @@ -0,0 +1,36 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.entity.chat.DirectChat; +import org.mercury_im.messenger.entity.chat.GroupChat; +import org.mercury_im.messenger.entity.message.Message; + +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.Single; + +public interface MessageRepository { + + Single insertMessage(DirectChat chat, Message message); + + Single insertMessage(GroupChat chat, Message message); + + Observable> observeMessages(DirectChat chat); + + Observable> observeMessages(GroupChat chat); + + Observable> findMessagesWithBody(String body); + + Observable> findMessagesWithBody(DirectChat chat, String body); + + Observable> findMessagesWithBody(GroupChat chat, String body); + + Single upsertMessage(DirectChat chat, Message message); + + Single upsertMessage(GroupChat chat, Message message); + + Single updateMessage(Message message); + + Completable deleteMessage(Message message); +} diff --git a/domain/src/main/java/org/mercury_im/messenger/data/repository/PeerRepository.java b/domain/src/main/java/org/mercury_im/messenger/data/repository/PeerRepository.java new file mode 100644 index 0000000..d3585c7 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/data/repository/PeerRepository.java @@ -0,0 +1,58 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.util.Optional; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.contact.Peer; + +import java.util.List; +import java.util.UUID; + +import io.reactivex.Completable; +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; + +public interface PeerRepository { + + Single insertPeer(Peer Peer); + + default Observable> observePeer(Peer peer) { + return observePeer(peer.getId()); + } + + Observable> observePeer(UUID PeerId); + + Maybe getPeer(UUID PeerId); + + default Observable> observePeerByAddress(Account account, String address) { + return observePeerByAddress(account.getId(), address); + } + + Observable> observePeerByAddress(UUID accountId, String address); + + default Maybe getPeerByAddress(Account account, String address) { + return getPeerByAddress(account.getId(), address); + } + + Maybe getPeerByAddress(UUID accountId, String address); + + Single getOrCreatePeer(Account account, String address); + + Single getOrCreatePeer(UUID accountId, String address); + + Observable> observeAllPeers(); + + default Observable> observeAllContactsOfAccount(Account account) { + return observeAllContactsOfAccount(account.getId()); + } + + Observable> observeAllContactsOfAccount(UUID accountId); + + Single updatePeer(Peer Peer); + + Single upsertPeer(Peer Peer); + + Completable deletePeer(Peer Peer); + + Completable deletePeer(UUID accountId, String address); +} diff --git a/domain/src/main/java/org/mercury_im/messenger/data/repository/Repositories.java b/domain/src/main/java/org/mercury_im/messenger/data/repository/Repositories.java new file mode 100644 index 0000000..bacdfd1 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/data/repository/Repositories.java @@ -0,0 +1,56 @@ +package org.mercury_im.messenger.data.repository; + +import org.mercury_im.messenger.entity.caps.EntityCapsRecord; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class Repositories { + + private final AccountRepository accountRepository; + private final DirectChatRepository directChatRepository; + private final GroupChatRepository groupChatRepository; + private final MessageRepository messageRepository; + private final PeerRepository peerRepository; + private final EntityCapsRepository entityCapsRepository; + + @Inject + public Repositories(AccountRepository accountRepository, + DirectChatRepository directChatRepository, + GroupChatRepository groupChatRepository, + MessageRepository messageRepository, + PeerRepository peerRepository, + EntityCapsRepository entityCapsRepository) { + this.accountRepository = accountRepository; + this.directChatRepository = directChatRepository; + this.groupChatRepository = groupChatRepository; + this.messageRepository = messageRepository; + this.peerRepository = peerRepository; + this.entityCapsRepository = entityCapsRepository; + } + + public AccountRepository getAccountRepository() { + return accountRepository; + } + + public DirectChatRepository getDirectChatRepository() { + return directChatRepository; + } + + public GroupChatRepository getGroupChatRepository() { + return groupChatRepository; + } + + public MessageRepository getMessageRepository() { + return messageRepository; + } + + public PeerRepository getPeerRepository() { + return peerRepository; + } + + public EntityCapsRepository getEntityCapsRepository() { + return entityCapsRepository; + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/exception/IllegalUsernameException.java b/domain/src/main/java/org/mercury_im/messenger/exception/IllegalUsernameException.java new file mode 100644 index 0000000..6e084dc --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/exception/IllegalUsernameException.java @@ -0,0 +1,8 @@ +package org.mercury_im.messenger.exception; + +public class IllegalUsernameException extends RuntimeException { + + public IllegalUsernameException(Throwable cause) { + super(cause); + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/listener/IncomingDirectMessageListener.java b/domain/src/main/java/org/mercury_im/messenger/listener/IncomingDirectMessageListener.java new file mode 100644 index 0000000..3facc87 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/listener/IncomingDirectMessageListener.java @@ -0,0 +1,11 @@ +package org.mercury_im.messenger.listener; + +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.chat.DirectChat; +import org.mercury_im.messenger.entity.message.Message; + +public interface IncomingDirectMessageListener { + + void onIncomingDirectMessage(Account account, DirectChat chat, Message message); + +} diff --git a/domain/src/main/java/org/mercury_im/messenger/listener/IncomingGroupChatMessageListener.java b/domain/src/main/java/org/mercury_im/messenger/listener/IncomingGroupChatMessageListener.java new file mode 100644 index 0000000..3fc9af3 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/listener/IncomingGroupChatMessageListener.java @@ -0,0 +1,11 @@ +package org.mercury_im.messenger.listener; + +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.chat.GroupChat; +import org.mercury_im.messenger.entity.message.Message; + +public interface IncomingGroupChatMessageListener { + + void onIncomingDirectMessage(Account account, GroupChat chat, Message message); + +} diff --git a/domain/src/main/java/org/mercury_im/messenger/listener/TypingEventListener.java b/domain/src/main/java/org/mercury_im/messenger/listener/TypingEventListener.java new file mode 100644 index 0000000..3d8e037 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/listener/TypingEventListener.java @@ -0,0 +1,9 @@ +package org.mercury_im.messenger.listener; + +import org.mercury_im.messenger.entity.chat.Chat; +import org.mercury_im.messenger.entity.event.TypingEvent; + +public interface TypingEventListener { + + void onTypingEventReceived(Chat chat, TypingEvent typingEvent); +} diff --git a/domain/src/main/java/org/mercury_im/messenger/logging/Tags.java b/domain/src/main/java/org/mercury_im/messenger/logging/Tags.java new file mode 100644 index 0000000..6ace309 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/logging/Tags.java @@ -0,0 +1,9 @@ +package org.mercury_im.messenger.logging; + +public class Tags { + + public static final String TAG_XMPP = "MercuryXMPP"; + public static final String TAG_DB = "MercuryDB"; + public static final String TAG_ANDROID = "MercuryAndroid"; + public static final String TAG_DOMAIN = "MercuryDomain"; +} diff --git a/domain/src/main/java/org/mercury_im/messenger/store/MercuryEntityCapsStore.java b/domain/src/main/java/org/mercury_im/messenger/store/MercuryEntityCapsStore.java new file mode 100644 index 0000000..10700a1 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/store/MercuryEntityCapsStore.java @@ -0,0 +1,63 @@ +package org.mercury_im.messenger.store; + +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.data.repository.EntityCapsRepository; +import org.mercury_im.messenger.entity.caps.EntityCapsRecord; +import org.mercury_im.messenger.entity.caps.IEntityCapsRecord; +import org.mercury_im.messenger.logging.Tags; + +import java.io.StringReader; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.disposables.CompositeDisposable; + +@Singleton +public class MercuryEntityCapsStore implements EntityCapsPersistentCache { + + private static final Logger LOGGER = Logger.getLogger(Tags.TAG_DOMAIN); + + private final CompositeDisposable disposable = new CompositeDisposable(); + private final EntityCapsRepository repository; + + @Inject + public MercuryEntityCapsStore(EntityCapsRepository entityCapsRepository) { + this.repository = entityCapsRepository; + } + + @Override + public void addDiscoverInfoByNodePersistent(String nodeVer, DiscoverInfo info) { + LOGGER.log(Level.INFO, "MercuryEntityCapsStore: addDiscoverInfoByNodePersistent: " + nodeVer); + EntityCapsRecord record = new IEntityCapsRecord(); + record.setNodeVer(nodeVer); + record.setXml(info.toXML().toString()); + + disposable.add(repository.insertEntityCapsRecord(record).subscribe()); + } + + @Override + public DiscoverInfo lookup(String nodeVer) { + LOGGER.log(Level.INFO, "MercuryEntityCapsStore: lookup: " + nodeVer); + return repository.maybeGetEntityCapsRecord(nodeVer) + .map(this::parseDiscoverInfo) + .onErrorComplete() + .blockingGet(); + } + + private DiscoverInfo parseDiscoverInfo(EntityCapsRecord record) throws Exception { + XmlPullParser parser = PacketParserUtils.getParserFor(new StringReader(record.getXml())); + return (DiscoverInfo) PacketParserUtils.parseIQ(parser); + } + + @Override + public void emptyCache() { + LOGGER.log(Level.INFO, "MercuryEntityCapsStore: emptyCache."); + // Not needed? + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/store/MercuryRosterStore.java b/domain/src/main/java/org/mercury_im/messenger/store/MercuryRosterStore.java new file mode 100644 index 0000000..63e9450 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/store/MercuryRosterStore.java @@ -0,0 +1,208 @@ +package org.mercury_im.messenger.store; + +import org.jivesoftware.smack.roster.packet.RosterPacket; +import org.jivesoftware.smack.roster.rosterstore.RosterStore; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; +import org.mercury_im.messenger.data.repository.AccountRepository; +import org.mercury_im.messenger.data.repository.PeerRepository; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.contact.Peer; +import org.mercury_im.messenger.entity.contact.SubscriptionDirection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; + +public class MercuryRosterStore implements RosterStore { + + private static final Logger LOGGER = Logger.getLogger(RosterStore.class.getName()); + + private final PeerRepository peerRepository; + private final AccountRepository accountRepository; + private Account account; + private final CompositeDisposable disposable = new CompositeDisposable(); + + private final Map itemMap = new HashMap<>(); + private String rosterVersion; + + public MercuryRosterStore(Account account, PeerRepository rosterRepository, AccountRepository accountRepository) { + this.account = account; + this.peerRepository = rosterRepository; + this.accountRepository = accountRepository; + LOGGER.log(Level.INFO, "Construct Roster Store for " + account.getId()); + } + + public void subscribe() { + disposable.add(peerRepository.observeAllContactsOfAccount(account) + .observeOn(Schedulers.computation()) + .subscribe(contactsList -> { + itemMap.clear(); + for (Peer contactModel : contactsList) { + itemMap.put(contactModel.getAddress(), fromEntity(contactModel)); + LOGGER.log(Level.INFO, "Populate itemMap with " + contactsList.size() + " items"); + + } + }, + error -> LOGGER.log(Level.WARNING, "An error occurred while updating roster cache", error))); + + /* + disposable.add(peerRepository.getRosterVersion(account) + .observeOn(Schedulers.computation()) + .subscribe( + result -> setRosterVersion(result), + error -> LOGGER.log(Level.WARNING, "An error occurred updating cached roster version", error))); + + */ + } + + public void unsubscribe() { + disposable.dispose(); + } + + private void setRosterVersion(String rosterVersion) { + this.rosterVersion = rosterVersion; + } + + @Override + public List getEntries() { + return new ArrayList<>(itemMap.values()); + } + + @Override + public RosterPacket.Item getEntry(Jid bareJid) { + return itemMap.get(bareJid.asUnescapedString()); + } + + @Override + public String getRosterVersion() { + return rosterVersion != null ? rosterVersion : ""; + } + + @Override + public boolean addEntry(RosterPacket.Item item, String version) { + writeEntryToDatabase(item); + writeRosterVersionToDatabase(version); + + return true; + } + + private void writeEntryToDatabase(RosterPacket.Item item) { + disposable.add(peerRepository.getOrCreatePeer(account, item.getJid().asUnescapedString()) + .map(peer -> toEntity(item, peer)) + .flatMap(peerRepository::upsertPeer) + .subscribe( + success -> LOGGER.log(Level.FINE, "Upserted contact model " + success + " successfully"), + error -> LOGGER.log(Level.WARNING, "An error occurred upserting contact " + item.getJid().asUnescapedString(), error) + )); + } + + private void writeRosterVersionToDatabase(String version) { + /* + disposable.add(peerRepository.updateRosterVersion(account, version) + .subscribe( + success -> LOGGER.log(Level.FINE, "Upserted roster version to " + rosterVersion + " successfully"), + error -> LOGGER.log(Level.WARNING, "An error occurred upserting roster version", error) + )); + */ + } + + @Override + public boolean resetEntries(Collection items, String version) { + LOGGER.log(Level.INFO, "Reset Entries: " + Arrays.toString(items.toArray())); + // Update database + // TODO: Delete other contacts + for (RosterPacket.Item item : items) { + writeEntryToDatabase(item); + } + + /* + disposable.add(peerRepository.updateRosterVersion(account, version) + .subscribe( + success -> LOGGER.log(Level.FINE, "Upserted roster version to " + rosterVersion + " successfully"), + error -> LOGGER.log(Level.WARNING, "An error occurred upserting roster version", error) + )); + */ + + return true; + } + + @Override + public boolean removeEntry(Jid bareJid, String version) { + LOGGER.log(Level.INFO, "Remove entry " + bareJid.toString()); + + disposable.add(peerRepository.deletePeer(account.getId(), bareJid.asEntityBareJidOrThrow().asEntityBareJidString()) + .subscribe( + () -> LOGGER.log(Level.FINE, "Deletion of contact " + bareJid.toString() + " successful"), + error -> LOGGER.log(Level.WARNING, "An error occurred deleting contact " + bareJid.toString(), error) + )); + /* + disposable.add(peerRepository.updateRosterVersion(account, version) + .subscribe( + success -> LOGGER.log(Level.FINE, "Upserted roster version to " + rosterVersion + " successfully"), + error -> LOGGER.log(Level.WARNING, "An error occurred upserting roster version", error) + )); + */ + return true; + } + + @Override + public void resetStore() { + LOGGER.log(Level.INFO, "Reset Store"); + + /* + disposable.add(peerRepository.deleteAllContactsOfAccount(account) + .subscribe( + success -> LOGGER.log(Level.FINE, "Successfully reset store."), + error -> LOGGER.log(Level.WARNING, "An error occurred resetting store", error) + )); + disposable.add(peerRepository.updateRosterVersion(account, "") + .subscribe( + success -> LOGGER.log(Level.FINE, "Successfully reset roster version"), + error -> LOGGER.log(Level.WARNING, "An error occurred resetting roster version", error) + )); + */ + } + + public RosterPacket.Item fromEntity(Peer contactModel) { + RosterPacket.Item item = new RosterPacket.Item( + JidCreate.entityBareFromOrThrowUnchecked(contactModel.getAddress()), + contactModel.getName()); + if (contactModel.getSubscriptionDirection() != null) { + item.setItemType(convert(contactModel.getSubscriptionDirection())); + } + item.setApproved(contactModel.isSubscriptionApproved()); + item.setSubscriptionPending(contactModel.isSubscriptionPending()); + return item; + } + + public Peer toEntity(RosterPacket.Item item, Peer peer) { + peer.setAccount(account); + peer.setAddress(item.getJid().asEntityBareJidOrThrow().asEntityBareJidString()); + peer.setName(item.getName()); + if (item.getItemType() != null) { + peer.setSubscriptionDirection(convert(item.getItemType())); + } + peer.setSubscriptionApproved(item.isApproved()); + peer.setSubscriptionPending(item.isSubscriptionPending()); + + return peer; + } + + public SubscriptionDirection convert(RosterPacket.ItemType type) { + return SubscriptionDirection.valueOf(type.toString()); + + } + + public RosterPacket.ItemType convert(SubscriptionDirection direction) { + return RosterPacket.ItemType.fromString(direction.toString()); + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/usecase/AddAccount.java b/domain/src/main/java/org/mercury_im/messenger/usecase/AddAccount.java new file mode 100644 index 0000000..87d0b46 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/usecase/AddAccount.java @@ -0,0 +1,82 @@ +package org.mercury_im.messenger.usecase; + +import org.mercury_im.messenger.data.repository.AccountRepository; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.xmpp.MercuryConnection; +import org.mercury_im.messenger.xmpp.MercuryConnectionManager; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.reactivex.Completable; +import io.reactivex.Single; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; + +public class AddAccount { + + private static final Logger LOGGER = Logger.getLogger(AddAccount.class.getName()); + + private final AccountRepository accountRepository; + private final MercuryConnectionManager connectionManager; + + private final CompositeDisposable disposable = new CompositeDisposable(); + + public AddAccount(AccountRepository accountRepository, MercuryConnectionManager connectionManager) { + this.accountRepository = accountRepository; + this.connectionManager = connectionManager; + } + + public Completable execute(Account account) { + return loginAndStoreAccountIfSuccessful(account); + } + + private Completable loginAndStoreAccountIfSuccessful(Account account) { + LOGGER.log(Level.INFO, "loginAndStoreIfSuccessful"); + return logIntoAccount(account).flatMap(connection -> + insertEnabledAccountIntoDatabase(account).flatMap(insertedAccount -> + updateAccountIdInConnectionSingle(insertedAccount, connection))) + .map(this::addConnectionToMessenger) + .ignoreElement(); + } + + private Single logIntoAccount(Account account) { + return getOrCreateConnectionSingle(account) + .doAfterSuccess(con -> LogIntoAccount.with(con).executeAndPossiblyThrow()) + .subscribeOn(Schedulers.io()); + } + + private Single getOrCreateConnectionSingle(Account account) { + return Single.fromCallable(() -> getOrCreateConnection(account)); + } + + private MercuryConnection getOrCreateConnection(Account account) { + MercuryConnection connection = connectionManager.getConnection(account); + if (connection == null) { + connection = new MercuryConnection(accountRepository, account); + } + return connection; + } + + private Single insertEnabledAccountIntoDatabase(Account account) { + account.setEnabled(true); + return accountRepository.upsertAccount(account); + } + + private Completable addConnectionToMessenger(MercuryConnection connection) { + return Completable.fromAction(() -> connectionManager.registerConnection(connection)); + } + + private Single updateAccountIdInConnectionSingle(Account account, MercuryConnection connection) { + return Single.fromCallable(() -> { + updateAccountIdInConnection(account, connection); + return connection; + }); + } + + private void updateAccountIdInConnection(Account account, MercuryConnection connection) { + if (connection != null) { + connection.getAccount().setId(account.getId()); + } + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/usecase/AddAccountUseCase.java b/domain/src/main/java/org/mercury_im/messenger/usecase/AddAccountUseCase.java new file mode 100644 index 0000000..6316d3d --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/usecase/AddAccountUseCase.java @@ -0,0 +1,17 @@ +package org.mercury_im.messenger.usecase; + +import io.reactivex.Completable; + +public interface AddAccountUseCase { + + AddAccountTask create(); + + Completable execute(AddAccountTask task); + + interface AddAccountTask { + + void setAddress(String address); + + void setPassword(String password); + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/usecase/LogIntoAccount.java b/domain/src/main/java/org/mercury_im/messenger/usecase/LogIntoAccount.java new file mode 100644 index 0000000..a3b74da --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/usecase/LogIntoAccount.java @@ -0,0 +1,74 @@ +package org.mercury_im.messenger.usecase; + +import org.jivesoftware.smack.AbstractXMPPConnection; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.sasl.SASLErrorException; +import org.mercury_im.messenger.xmpp.MercuryConnection; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.reactivex.Completable; +import io.reactivex.Single; + +public class LogIntoAccount { + + public enum ConnectionResult { + success, + credential_error, + server_error, + other_error + } + + private static final Logger LOGGER = Logger.getLogger(LogIntoAccount.class.getName()); + + private final MercuryConnection connection; + + private LogIntoAccount(MercuryConnection connection) { + this.connection = connection; + } + + public static LogIntoAccount with(MercuryConnection connection) { + if (connection == null) { + throw new NullPointerException("MercuryConnection cannot be null."); + } + return new LogIntoAccount(connection); + } + + public Completable executeAndPossiblyThrow() { + return Completable.fromAction(this::doAuthenticateIfNecessary); + } + + public Single execute() { + return Single.fromCallable(this::authenticateIfNecessary); + } + + private ConnectionResult authenticateIfNecessary() { + try { + doAuthenticateIfNecessary(); + return ConnectionResult.success; + } catch (SASLErrorException e) { + LOGGER.log(Level.WARNING, "SASL Error while connecting to account " + connection.getAccount().getAddress(), e); + return ConnectionResult.credential_error; + } catch (SmackException.ConnectionException e) { + LOGGER.log(Level.WARNING, "Connectivity error while connecting to account " + connection.getAccount().getAddress(), e); + return ConnectionResult.server_error; + } + catch (IOException | XMPPException | SmackException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Error connecting to account " + connection.getAccount().getAddress(), e); + return ConnectionResult.other_error; + } + } + + private void doAuthenticateIfNecessary() + throws InterruptedException, XMPPException, SmackException, IOException { + if (!connection.getConnection().isAuthenticated()) { + LOGGER.log(Level.INFO, "Logging in"); + ((AbstractXMPPConnection) connection.getConnection()).connect().login(); + LOGGER.log(Level.INFO, "Login complete"); + } + } + +} diff --git a/domain/src/main/java/org/mercury_im/messenger/usecase/RosterStoreBinder.java b/domain/src/main/java/org/mercury_im/messenger/usecase/RosterStoreBinder.java new file mode 100644 index 0000000..795916f --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/usecase/RosterStoreBinder.java @@ -0,0 +1,33 @@ +package org.mercury_im.messenger.usecase; + +import org.jivesoftware.smack.roster.Roster; +import org.mercury_im.messenger.data.repository.AccountRepository; +import org.mercury_im.messenger.data.repository.PeerRepository; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.store.MercuryRosterStore; +import org.mercury_im.messenger.xmpp.MercuryConnection; + +import javax.inject.Inject; + +public class RosterStoreBinder { + + private final AccountRepository accountRepository; + private final PeerRepository peerRepository; + + @Inject + public RosterStoreBinder(AccountRepository accountRepository, PeerRepository peerRepository) { + this.accountRepository = accountRepository; + this.peerRepository = peerRepository; + } + + public void setRosterStoreOn(MercuryConnection connection) { + MercuryRosterStore store = + createRosterStore(connection.getAccount(), accountRepository, peerRepository); + Roster roster = Roster.getInstanceFor(connection.getConnection()); + roster.setRosterStore(store); + } + + private MercuryRosterStore createRosterStore(Account account, AccountRepository accountRepository, PeerRepository peerRepository) { + return new MercuryRosterStore(account, peerRepository, accountRepository); + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/util/DiffUtil.java b/domain/src/main/java/org/mercury_im/messenger/util/DiffUtil.java new file mode 100644 index 0000000..ee472d7 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/util/DiffUtil.java @@ -0,0 +1,57 @@ +package org.mercury_im.messenger.util; + +import java.util.ArrayList; +import java.util.List; + +public class DiffUtil { + + public static Diff diff(List before, List after) { + List removed = getRemovedItems(before, after); + List added = getAddedItems(before, after); + List retained = getRetainedItems(before, after); + return new Diff<>(added, retained, removed); + } + + private static List getRemovedItems(List before, List after) { + List withoutRetained = new ArrayList<>(before); + withoutRetained.removeAll(after); + return withoutRetained; + } + + private static List getAddedItems(List before, List after) { + List withoutPrevious = new ArrayList<>(after); + withoutPrevious.removeAll(before); + return withoutPrevious; + } + + private static List getRetainedItems(List before, List after) { + List retained = new ArrayList<>(before); + retained.retainAll(after); + return retained; + } + + public static class Diff { + + private final List added; + private final List retained; + private final List removed; + + public Diff(List added, List retained, List removed) { + this.added = added; + this.retained = retained; + this.removed = removed; + } + + public List getAdded() { + return added; + } + + public List getRetained() { + return retained; + } + + public List getRemoved() { + return removed; + } + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/util/Optional.java b/domain/src/main/java/org/mercury_im/messenger/util/Optional.java new file mode 100644 index 0000000..4fae342 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/util/Optional.java @@ -0,0 +1,27 @@ +package org.mercury_im.messenger.util; + +/** + * Since j.u.Optional is only available on Android since API lvl 24, we need this utility class. + * + * @param type of wrapped object. + */ +public class Optional { + + private final T item; + + public Optional() { + this(null); + } + + public Optional(T item) { + this.item = item; + } + + public T getItem() { + return item; + } + + public boolean isPresent() { + return item != null; + } +} diff --git a/thread_utils/src/main/java/org/mercury_im/messenger/thread_utils/ThreadUtils.java b/domain/src/main/java/org/mercury_im/messenger/util/ThreadUtils.java similarity index 68% rename from thread_utils/src/main/java/org/mercury_im/messenger/thread_utils/ThreadUtils.java rename to domain/src/main/java/org/mercury_im/messenger/util/ThreadUtils.java index 08d5e31..d630b73 100644 --- a/thread_utils/src/main/java/org/mercury_im/messenger/thread_utils/ThreadUtils.java +++ b/domain/src/main/java/org/mercury_im/messenger/util/ThreadUtils.java @@ -1,7 +1,7 @@ -package org.mercury_im.messenger.thread_utils; +package org.mercury_im.messenger.util; /** - * Name constants used by dagger in combination with the @Named annotation. + * Names for identifying scheduler instances during dependency injection. */ public class ThreadUtils { /** diff --git a/domain/src/main/java/org/mercury_im/messenger/xmpp/MercuryConnection.java b/domain/src/main/java/org/mercury_im/messenger/xmpp/MercuryConnection.java new file mode 100644 index 0000000..1cadeff --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/xmpp/MercuryConnection.java @@ -0,0 +1,104 @@ +package org.mercury_im.messenger.xmpp; + +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.XMPPConnection; +import org.mercury_im.messenger.data.repository.AccountRepository; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.util.Optional; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.reactivex.Observable; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.subjects.BehaviorSubject; + +public class MercuryConnection { + + private static final Logger LOGGER = Logger.getLogger("MercuryConnection"); + private static final XmppConnectionFactory connectionFactory = new XmppConnectionFactory(); + + private final CompositeDisposable disposable = new CompositeDisposable(); + private final AccountRepository accountRepository; + + private Account account; + private XMPPConnection connection; + + private BehaviorSubject enabled = BehaviorSubject.create(); + private BehaviorSubject state = BehaviorSubject.createDefault(ConnectionState.closed); + + public MercuryConnection(AccountRepository accountRepository, Account account) { + this.accountRepository = accountRepository; + this.account = account; + this.connection = connectionFactory.createConnection(account); + observeAccountAndConnection(); + } + + public Observable getState() { + return state; + } + + private void observeAccountAndConnection() { + observeAccount(); + observeConnection(); + } + + private void observeAccount() { + disposable.add(accountRepository.observeAccount(account.getId()) + .filter(Optional::isPresent) + .map(Optional::getItem) + .subscribe(this::setAccount)); + } + + private void observeConnection() { + connection.addConnectionListener(new ConnectionListener() { + @Override + public void connected(XMPPConnection connection) { + LOGGER.log(Level.FINER, "connected"); + state.onNext(ConnectionState.connected); + } + + @Override + public void authenticated(XMPPConnection connection, boolean resumed) { + LOGGER.log(Level.FINER, "authenticated. resumed? " + resumed); + state.onNext(ConnectionState.authenticated); + } + + @Override + public void connectionClosed() { + LOGGER.log(Level.FINER, "connectionClosed"); + state.onNext(ConnectionState.closed); + } + + @Override + public void connectionClosedOnError(Exception e) { + LOGGER.log(Level.WARNING, "connectionClosedOnError"); + state.onNext(ConnectionState.closedOnError); + } + }); + } + + private void setAccount(Account account) { + this.account = account; + enabled.onNext(account.isEnabled()); + } + + public Account getAccount() { + return account; + } + + public XMPPConnection getConnection() { + return connection; + } + + public void dispose() { + disposable.dispose(); + } + + public enum ConnectionState { + connected, + authenticated, + closedOnError, + closed + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/xmpp/MercuryConnectionManager.java b/domain/src/main/java/org/mercury_im/messenger/xmpp/MercuryConnectionManager.java new file mode 100644 index 0000000..2b3a6c9 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/xmpp/MercuryConnectionManager.java @@ -0,0 +1,173 @@ +package org.mercury_im.messenger.xmpp; + +import org.jivesoftware.smack.AbstractXMPPConnection; +import org.jivesoftware.smack.ReconnectionListener; +import org.jivesoftware.smack.ReconnectionManager; +import org.jivesoftware.smackx.caps.EntityCapsManager; +import org.mercury_im.messenger.data.repository.AccountRepository; +import org.mercury_im.messenger.data.repository.Repositories; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.store.MercuryEntityCapsStore; +import org.mercury_im.messenger.usecase.LogIntoAccount; +import org.mercury_im.messenger.usecase.RosterStoreBinder; +import org.mercury_im.messenger.util.Optional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import io.reactivex.Observable; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.BehaviorSubject; + +@Singleton +public class MercuryConnectionManager { + + private static final Logger LOGGER = Logger.getLogger("ConnectionManager"); + + private final Map connections = new ConcurrentHashMap<>(); + private final BehaviorSubject> connectionsSubject = BehaviorSubject.createDefault(connections); + + private final AccountRepository accountRepository; + + private final RosterStoreBinder rosterStoreBinder; + private final MercuryEntityCapsStore entityCapsStore; + + private final CompositeDisposable disposable = new CompositeDisposable(); + + static { + ReconnectionManager.setEnabledPerDefault(true); + ReconnectionManager.setDefaultReconnectionPolicy(ReconnectionManager.ReconnectionPolicy.RANDOM_INCREASING_DELAY); + } + + @Inject + public MercuryConnectionManager(Repositories repositories, + RosterStoreBinder rosterStoreBinder, + MercuryEntityCapsStore entityCapsStore) { + this.accountRepository = repositories.getAccountRepository(); + this.rosterStoreBinder = rosterStoreBinder; + this.entityCapsStore = entityCapsStore; + + EntityCapsManager.setPersistentCache(entityCapsStore); + } + + public List getConnections() { + return new ArrayList<>(connections.values()); + } + + public Observable> observeConnections() { + return connectionsSubject; + } + + public MercuryConnection getConnection(Account account) { + return getConnection(account.getId()); + } + + public MercuryConnection getConnection(UUID id) { + return connections.get(id); + } + + public void registerConnections(List accounts) { + for (Account account : accounts) { + MercuryConnection connection = new MercuryConnection(accountRepository, account); + registerConnection(connection); + } + } + + public void registerConnection(MercuryConnection connection) { + LOGGER.log(Level.INFO, "Register Connection " + connection.getAccount().getAddress()); + putConnection(connection); + disposable.add(accountRepository + .observeAccount(connection.getAccount().getId()) + .subscribeOn(Schedulers.newThread()) + .observeOn(Schedulers.newThread()) + .subscribe(event -> + handleOptionalAccountChangedEvent(connection, event))); + } + + private void putConnection(MercuryConnection connection) { + connections.put(connection.getAccount().getId(), connection); + connectionsSubject.onNext(connections); + bindConnection(connection); + } + + public void bindConnection(MercuryConnection connection) { + rosterStoreBinder.setRosterStoreOn(connection); + ReconnectionManager.getInstanceFor((AbstractXMPPConnection) connection.getConnection()) + .addReconnectionListener(new ReconnectionListener() { + @Override + public void reconnectingIn(int seconds) { + LOGGER.log(Level.FINER, "Reconnecting connection " + connection.getAccount().getAddress() + " in " + seconds + " seconds."); + } + + @Override + public void reconnectionFailed(Exception e) { + LOGGER.log(Level.WARNING, "Reconnection of connection " + connection.getAccount().getAddress() + " failed.", e); + } + }); + } + + private void handleOptionalAccountChangedEvent(MercuryConnection connection, Optional event) { + if (event.isPresent()) { + handleAccountChangedEvent(connection, event.getItem()); + } else { + handleAccountRemoved(connection); + } + } + + private void handleAccountChangedEvent(MercuryConnection connection, Account account) { + if (account.isEnabled()) { + handleAccountEnabled(connection); + } else { + handleAccountDisabled(connection); + } + } + + private void handleAccountDisabled(MercuryConnection connection) { + LOGGER.log(Level.FINER, "HandleAccountDisabled: " + connection.getAccount().getAddress()); + connectionDisconnect(connection); + } + + private void handleAccountEnabled(MercuryConnection connection) { + LOGGER.log(Level.FINER, "HandleAccountEnabled: " + connection.getAccount().getAddress()); + connectionLogin(connection); + } + + private void connectionLogin(MercuryConnection connection) { + disposable.add(LogIntoAccount.with(connection).executeAndPossiblyThrow() + .subscribeOn(Schedulers.newThread()) + .subscribe(() -> LOGGER.log(Level.FINER, "Logged in."), + error -> LOGGER.log(Level.SEVERE, "Connection error!", error))); + } + + private void handleAccountRemoved(MercuryConnection connection) { + disconnectAndRemoveConnection(connection); + } + + private void disconnectAndRemoveConnection(MercuryConnection connection) { + connectionDisconnect(connection); + removeConnection(connection); + } + + private void connectionDisconnect(MercuryConnection connection) { + if (connection.getConnection().isAuthenticated()) { + ((AbstractXMPPConnection) connection.getConnection()).disconnect(); + } + } + + private void removeConnection(MercuryConnection connection) { + LOGGER.log(Level.FINER, "Remove Connection: " + connection.getAccount().getAddress()); + connections.remove(connection.getAccount().getId()); + connectionsSubject.onNext(connections); + connection.dispose(); + } + +} diff --git a/domain/src/main/java/org/mercury_im/messenger/xmpp/XmppConnectionFactory.java b/domain/src/main/java/org/mercury_im/messenger/xmpp/XmppConnectionFactory.java new file mode 100644 index 0000000..f1da4d9 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/xmpp/XmppConnectionFactory.java @@ -0,0 +1,35 @@ +package org.mercury_im.messenger.xmpp; + +import org.jivesoftware.smack.AbstractXMPPConnection; +import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.stringprep.XmppStringprepException; +import org.mercury_im.messenger.entity.Account; + +public class XmppConnectionFactory { + + private static final int DEFAULT_PORT = 5222; + private static final int CONNECTION_TIMEOUT = 30 * 1000; + + public AbstractXMPPConnection createConnection(Account account) { + try { + return new XMPPTCPConnection(XMPPTCPConnectionConfiguration.builder() + .setConnectTimeout(CONNECTION_TIMEOUT) + .setXmppAddressAndPassword(account.getAddress(), account.getPassword()) + .setHost(determineHost(account)) + .setPort(determinePort(account)) + .build()); + } catch (XmppStringprepException e) { + throw new AssertionError("Account has invalid address: " + account.getAddress(), e); + } + } + + private String determineHost(Account account) throws XmppStringprepException { + return account.getHost() != null ? account.getHost() : JidCreate.domainBareFrom(account.getAddress()).toString(); + } + + private int determinePort(Account account) { + return account.getPort() != 0 ? account.getPort() : DEFAULT_PORT; + } +} diff --git a/domain/src/main/java/org/mercury_im/messenger/xmpp/XmppDirectMessageCenter.java b/domain/src/main/java/org/mercury_im/messenger/xmpp/XmppDirectMessageCenter.java new file mode 100644 index 0000000..35b8f30 --- /dev/null +++ b/domain/src/main/java/org/mercury_im/messenger/xmpp/XmppDirectMessageCenter.java @@ -0,0 +1,119 @@ +package org.mercury_im.messenger.xmpp; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.chat2.Chat; +import org.jivesoftware.smack.chat2.ChatManager; +import org.jivesoftware.smack.chat2.IncomingChatMessageListener; +import org.jivesoftware.smackx.sid.element.OriginIdElement; +import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.mercury_im.messenger.MessageCenter; +import org.mercury_im.messenger.Messenger; +import org.mercury_im.messenger.data.repository.AccountRepository; +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.data.repository.Repositories; +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.chat.DirectChat; +import org.mercury_im.messenger.entity.message.IMessage; +import org.mercury_im.messenger.entity.message.IMessageMetadata; +import org.mercury_im.messenger.entity.message.Message; +import org.mercury_im.messenger.listener.IncomingDirectMessageListener; + +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.inject.Inject; + +import io.reactivex.Completable; +import io.reactivex.disposables.CompositeDisposable; + +public class XmppDirectMessageCenter + implements MessageCenter, IncomingChatMessageListener { + + private final PeerRepository peerRepository; + private final AccountRepository accountRepository; + private final DirectChatRepository directChatRepository; + private final MessageRepository messageRepository; + + private final Messenger messenger; + private final Account account; + + private final CompositeDisposable disposable = new CompositeDisposable(); + + private Set messageListeners = new LinkedHashSet<>(); + + @Inject + public XmppDirectMessageCenter(Account account, Messenger messenger, Repositories repositories) { + this.messenger = messenger; + this.account = account; + this.peerRepository = repositories.getPeerRepository(); + this.accountRepository = repositories.getAccountRepository(); + this.directChatRepository = repositories.getDirectChatRepository(); + this.messageRepository = repositories.getMessageRepository(); + + XMPPConnection connection = getMessenger().getConnectionManager().getConnection(account).getConnection(); + + ChatManager.getInstanceFor(connection).addIncomingListener(this); + } + + @Override + public Messenger getMessenger() { + return messenger; + } + + @Override + public Completable sendMessage(Message message, DirectChat chat) { + ChatManager chatManager = getChatManager(chat); + + EntityBareJid accountAddress = + JidCreate.entityBareFromOrThrowUnchecked(chat.getAccount().getAddress()); + EntityBareJid peerAddress = + JidCreate.entityBareFromOrThrowUnchecked(chat.getPeer().getAddress()); + + org.jivesoftware.smack.packet.Message smackMessage = new org.jivesoftware.smack.packet.Message(); + smackMessage.setFrom(accountAddress); + smackMessage.setTo(peerAddress); + smackMessage.setType(org.jivesoftware.smack.packet.Message.Type.chat); + + String originId = OriginIdElement.addOriginId(smackMessage).getId(); + String legacyStanzaId = smackMessage.getStanzaId(); + + IMessageMetadata metadata = new IMessageMetadata(); + metadata.setLegacyStanzaId(legacyStanzaId); + metadata.setOriginId(originId); + + message.setMetadata(metadata); + + Chat smackChat = chatManager.chatWith(peerAddress); + return messageRepository.insertMessage(chat, message) + .ignoreElement() + .andThen(Completable.fromAction(() -> smackChat.send(smackMessage))); + } + + @Override + public void addIncomingMessageListener(IncomingDirectMessageListener listener) { + messageListeners.add(listener); + } + + protected ChatManager getChatManager(DirectChat chat) { + MercuryConnection mercuryConnection = getMessenger().getConnectionManager().getConnection(chat.getAccount()); + return ChatManager.getInstanceFor(mercuryConnection.getConnection()); + } + + @Override + public void newIncomingMessage(EntityBareJid from, org.jivesoftware.smack.packet.Message message, Chat chat) { + disposable.add(peerRepository + // get peer + .getOrCreatePeer(account, from.asEntityBareJidString()) + // get chat + .flatMap(peer -> directChatRepository.getOrCreateChatWithPeer(peer)) + // notify listeners + .subscribe(chatEntity -> { + for (IncomingDirectMessageListener listener : messageListeners) { + listener.onIncomingDirectMessage(account, chatEntity, new IMessage()); + } + })); + } +} diff --git a/domain/src/test/java/org/mercury_im/messenger/learning_tests/smack/AddAccountUseCaseTest.java b/domain/src/test/java/org/mercury_im/messenger/learning_tests/smack/AddAccountUseCaseTest.java new file mode 100644 index 0000000..cf41f75 --- /dev/null +++ b/domain/src/test/java/org/mercury_im/messenger/learning_tests/smack/AddAccountUseCaseTest.java @@ -0,0 +1,17 @@ +package org.mercury_im.messenger.learning_tests.smack; + +import org.junit.Test; +import org.mercury_im.messenger.usecase.AddAccountUseCase; + +import java.util.UUID; + +public class AddAccountUseCaseTest { + + @Test + public void test() { + AddAccountUseCase useCase; + AddAccountUseCase.AddAccountTask task; + + System.out.println(UUID.randomUUID()); + } +} diff --git a/domain/src/test/java/org/mercury_im/messenger/learning_tests/smack/XmppConnectionLoginBehaviorTest.java b/domain/src/test/java/org/mercury_im/messenger/learning_tests/smack/XmppConnectionLoginBehaviorTest.java new file mode 100644 index 0000000..2b62017 --- /dev/null +++ b/domain/src/test/java/org/mercury_im/messenger/learning_tests/smack/XmppConnectionLoginBehaviorTest.java @@ -0,0 +1,91 @@ +package org.mercury_im.messenger.learning_tests.smack; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.sasl.SASLErrorException; +import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.util.stringencoder.Base64; +import org.jivesoftware.smack.util.stringencoder.java7.Java7Base64Encoder; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.Assume.assumeTrue; + +/** + * Learning Test to study Smacks behavior during connection process. + */ +public class XmppConnectionLoginBehaviorTest { + + public static final String CREDENTIALS_PROPERTIES = "testcredentials.properties"; + + private static final Logger LOGGER = Logger.getLogger(XmppConnectionLoginBehaviorTest.class.getName()); + private static Properties testCredentials = null; + + @BeforeClass + public static void readProperties() { + Properties properties = new Properties(); + File propertiesFile = new File(CREDENTIALS_PROPERTIES); + if (!propertiesFile.exists() || !propertiesFile.isFile()) { + LOGGER.log(Level.WARNING, "Cannot find file domain/testcredentials.properties. Some tests will be skipped."); + return; + } + + try(FileReader reader = new FileReader(propertiesFile)) { + properties.load(reader); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error reading properties file testcredentials.properties.", e); + return; + } + testCredentials = properties; + + Base64.setEncoder(Java7Base64Encoder.getInstance()); + } + + /* + * Connecting to an invalid host causes {@link org.jivesoftware.smack.SmackException.ConnectionException} + * to be thrown. + */ + @Test(expected = SmackException.ConnectionException.class) + public void invalidHostConnectionTest() throws IOException, InterruptedException, XMPPException, SmackException { + ignoreIfNoCredentials(); + new XMPPTCPConnection( + testCredentials.getProperty("invalidHostUsername"), + testCredentials.getProperty("invalidHostPassword")) + .connect().login(); + } + + /* + * Connecting with invalid user causes {@link SASLErrorException} to be thrown. + */ + @Test(expected = SASLErrorException.class) + public void invalidUserConnectionTest() throws IOException, InterruptedException, XMPPException, SmackException { + ignoreIfNoCredentials(); + new XMPPTCPConnection( + testCredentials.getProperty("invalidUserUsername"), + testCredentials.getProperty("invalidUserPassword")) + .connect().login(); + } + + /* + * Connecting with invalid password causes {@link SASLErrorException} to be thrown. + */ + @Test(expected = SASLErrorException.class) + public void invalidPasswordConnectionTest() throws IOException, InterruptedException, XMPPException, SmackException { + ignoreIfNoCredentials(); + new XMPPTCPConnection( + testCredentials.getProperty("invalidPasswordUsername"), + testCredentials.getProperty("invalidPasswordPassword")) + .connect().login(); + } + + private void ignoreIfNoCredentials() { + assumeTrue("Test ignored as domain/testcredentials.properties file not found.", testCredentials != null); + } +} diff --git a/domain/testcredentials.properties.example b/domain/testcredentials.properties.example new file mode 100644 index 0000000..2752843 --- /dev/null +++ b/domain/testcredentials.properties.example @@ -0,0 +1,11 @@ +#Server must not exist +invalidHostUsername=invalid@example.com +invalidHostPassword=invalid123 + +#User must not exist on a server that is reachable +invalidUserUsername= +invalidPassword= + +#User must exists, but password must be invalid +invalidPasswordUsername= +invalidPasswordPassword= diff --git a/thread_utils/.gitignore b/entity/.gitignore similarity index 100% rename from thread_utils/.gitignore rename to entity/.gitignore diff --git a/entity/README.md b/entity/README.md new file mode 100644 index 0000000..0a1719a --- /dev/null +++ b/entity/README.md @@ -0,0 +1,5 @@ +# Entity + +This module contains entity definitions for MercuryIM. +According to Robert C. Martins "Clean Architecture", entities form the inner most layer of the +program. \ No newline at end of file diff --git a/thread_utils/build.gradle b/entity/build.gradle similarity index 62% rename from thread_utils/build.gradle rename to entity/build.gradle index 82292e2..9029aa4 100644 --- a/thread_utils/build.gradle +++ b/entity/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'java-library' dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) + } sourceCompatibility = "8" diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/Account.java b/entity/src/main/java/org/mercury_im/messenger/entity/Account.java new file mode 100644 index 0000000..e1e52c2 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/Account.java @@ -0,0 +1,41 @@ +package org.mercury_im.messenger.entity; + +import java.util.UUID; + +/** + * User Account entity. + * + * An implementation of this entity can be found as {@link IAccount}. + */ +public interface Account { + + UUID UNASSIGNED = UUID.fromString("00000000-0000-0000-0000-000000000000"); + + void setId(UUID id); + + UUID getId(); + + void setAddress(String address); + + String getAddress(); + + void setPassword(String password); + + String getPassword(); + + void setHost(String host); + + String getHost(); + + void setPort(int port); + + int getPort(); + + void setEnabled(boolean enabled); + + boolean isEnabled(); + + default String displayName() { + return getAddress(); + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/IAccount.java b/entity/src/main/java/org/mercury_im/messenger/entity/IAccount.java new file mode 100644 index 0000000..7c22ac8 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/IAccount.java @@ -0,0 +1,85 @@ +package org.mercury_im.messenger.entity; + +import java.util.UUID; + +public class IAccount implements Account { + + protected UUID id; + protected String address; + protected String password; + protected String host; + protected int port; + protected boolean enabled; + + public IAccount() { + this(UUID.randomUUID()); + } + + public IAccount(UUID id) { + this.id = id; + } + + public static Account createUnassignedAccount() { + return new IAccount(Account.UNASSIGNED); + } + + @Override + public void setId(UUID id) { + this.id = id; + } + + @Override + public UUID getId() { + return id; + } + + @Override + public void setAddress(String address) { + this.address = address; + } + + @Override + public String getAddress() { + return address; + } + + @Override + public void setPassword(String password) { + this.password = password; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public void setHost(String host) { + this.host = host; + } + + @Override + public String getHost() { + return host; + } + + @Override + public void setPort(int port) { + this.port = port; + } + + @Override + public int getPort() { + return port; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean isEnabled() { + return enabled; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/caps/EntityCapsRecord.java b/entity/src/main/java/org/mercury_im/messenger/entity/caps/EntityCapsRecord.java new file mode 100644 index 0000000..bc3ad16 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/caps/EntityCapsRecord.java @@ -0,0 +1,12 @@ +package org.mercury_im.messenger.entity.caps; + +public interface EntityCapsRecord { + + String getNodeVer(); + + void setNodeVer(String nodeVer); + + String getXml(); + + void setXml(String xml); +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/caps/IEntityCapsRecord.java b/entity/src/main/java/org/mercury_im/messenger/entity/caps/IEntityCapsRecord.java new file mode 100644 index 0000000..c740cf5 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/caps/IEntityCapsRecord.java @@ -0,0 +1,27 @@ +package org.mercury_im.messenger.entity.caps; + +public class IEntityCapsRecord implements EntityCapsRecord { + + private String nodeVer; + private String xml; + + @Override + public String getNodeVer() { + return nodeVer; + } + + @Override + public void setNodeVer(String nodeVer) { + this.nodeVer = nodeVer; + } + + @Override + public String getXml() { + return xml; + } + + @Override + public void setXml(String xml) { + this.xml = xml; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/chat/Chat.java b/entity/src/main/java/org/mercury_im/messenger/entity/chat/Chat.java new file mode 100644 index 0000000..a100aba --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/chat/Chat.java @@ -0,0 +1,25 @@ +package org.mercury_im.messenger.entity.chat; + +import org.mercury_im.messenger.entity.Account; + +import java.util.UUID; + +/** + * Generic interface defining shared properties of chats. + * + * Child interfaces of {@link Chat} are {@link DirectChat} and {@link GroupChat}. + */ +public interface Chat { + + UUID getId(); + + void setId(UUID id); + + Account getAccount(); + + void setAccount(Account account); + + ChatPreferences getChatPreferences(); + + void setChatPreferences(ChatPreferences chatPreferences); +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/chat/ChatPreferences.java b/entity/src/main/java/org/mercury_im/messenger/entity/chat/ChatPreferences.java new file mode 100644 index 0000000..f6aee3c --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/chat/ChatPreferences.java @@ -0,0 +1,41 @@ +package org.mercury_im.messenger.entity.chat; + +/** + * Interface that describes typical preferences in the context of a chat. + * + * An implementation can be found as {@link IChatPreferences}. + */ +public interface ChatPreferences { + + NotificationPreferences getNotificationPreference(); + + void setNotificationPreference(NotificationPreferences notificationPreferences); + + boolean isTypingNotificationsSupported(); + + void setTypingNotificationsSupported(boolean typingNotificationsSupported); + + boolean isSendTypingNotificationsEnabled(); + + void setSendTypingNotificationsEnabled(boolean sendTypingNotifications); + + boolean isReadNotificationsSupported(); + + void setReadNotificationsSupported(boolean readNotificationsSupported); + + boolean isSendReadNotifications(); + + void setSendReadNotifications(boolean sendReadNotifications); + + + interface NotificationPreferences { + + boolean isNotifyOnMessage(); + + void setNotifyOnMessage(boolean notify); + + boolean isNotifyOnMention(); + + void setNotifyOnMention(boolean notify); + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/chat/DirectChat.java b/entity/src/main/java/org/mercury_im/messenger/entity/chat/DirectChat.java new file mode 100644 index 0000000..b831e4a --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/chat/DirectChat.java @@ -0,0 +1,16 @@ +package org.mercury_im.messenger.entity.chat; + +import org.mercury_im.messenger.entity.contact.Peer; + +/** + * Interface that describes a direct chat between the user and another one. + * + * An implementation can be found in {@link IDirectChat}. + */ +public interface DirectChat extends Chat { + + Peer getPeer(); + + void setPeer(Peer peer); + +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/chat/GroupChat.java b/entity/src/main/java/org/mercury_im/messenger/entity/chat/GroupChat.java new file mode 100644 index 0000000..feae5dd --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/chat/GroupChat.java @@ -0,0 +1,25 @@ +package org.mercury_im.messenger.entity.chat; + +import org.mercury_im.messenger.entity.contact.Peer; + +import java.util.Set; + +/** + * An interface that describes a group chat entity. + * + * An implementation can be found as {@link IGroupChat}. + */ +public interface GroupChat extends Chat { + + Set getParticipants(); + + void setParticipants(Set participants); + + String getRoomAddress(); + + void setRoomAddress(String roomAddress); + + String getRoomName(); + + void setRoomName(String roomName); +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/chat/IChatPreferences.java b/entity/src/main/java/org/mercury_im/messenger/entity/chat/IChatPreferences.java new file mode 100644 index 0000000..adc1020 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/chat/IChatPreferences.java @@ -0,0 +1,60 @@ +package org.mercury_im.messenger.entity.chat; + +public class IChatPreferences implements ChatPreferences { + + protected NotificationPreferences notificationPreferences; + protected boolean typingNotificationsSupported; + protected boolean sendTypingNotificationsEnabled; + protected boolean readNotificationsSupported; + protected boolean sendReadNotificationsEnabled; + + @Override + public NotificationPreferences getNotificationPreference() { + return notificationPreferences; + } + + @Override + public void setNotificationPreference(NotificationPreferences notificationPreferences) { + this.notificationPreferences = notificationPreferences; + } + + @Override + public boolean isTypingNotificationsSupported() { + return typingNotificationsSupported; + } + + @Override + public void setTypingNotificationsSupported(boolean typingNotificationsSupported) { + this.typingNotificationsSupported = typingNotificationsSupported; + } + + @Override + public boolean isSendTypingNotificationsEnabled() { + return sendTypingNotificationsEnabled; + } + + @Override + public void setSendTypingNotificationsEnabled(boolean sendTypingNotifications) { + this.sendTypingNotificationsEnabled = sendTypingNotifications; + } + + @Override + public boolean isReadNotificationsSupported() { + return readNotificationsSupported; + } + + @Override + public void setReadNotificationsSupported(boolean readNotificationsSupported) { + this.readNotificationsSupported = readNotificationsSupported; + } + + @Override + public boolean isSendReadNotifications() { + return sendReadNotificationsEnabled; + } + + @Override + public void setSendReadNotifications(boolean sendReadNotifications) { + this.sendReadNotificationsEnabled = sendReadNotifications; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/chat/IDirectChat.java b/entity/src/main/java/org/mercury_im/messenger/entity/chat/IDirectChat.java new file mode 100644 index 0000000..dd4b78c --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/chat/IDirectChat.java @@ -0,0 +1,62 @@ +package org.mercury_im.messenger.entity.chat; + +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.contact.Peer; + +import java.util.UUID; + +public class IDirectChat implements DirectChat { + + protected UUID id; + protected Peer peer; + protected Account account; + protected ChatPreferences preferences; + + public IDirectChat() { + this(UUID.randomUUID()); + } + + public IDirectChat(UUID id) { + this.id = id; + } + + @Override + public Peer getPeer() { + return peer; + } + + @Override + public void setPeer(Peer peer) { + this.peer = peer; + } + + @Override + public UUID getId() { + return id; + } + + @Override + public void setId(UUID id) { + this.id = id; + } + + @Override + public Account getAccount() { + return account; + } + + @Override + public void setAccount(Account account) { + this.account = account; + } + + @Override + public ChatPreferences getChatPreferences() { + return preferences; + } + + @Override + public void setChatPreferences(ChatPreferences chatPreferences) { + this.preferences = chatPreferences; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/chat/IGroupChat.java b/entity/src/main/java/org/mercury_im/messenger/entity/chat/IGroupChat.java new file mode 100644 index 0000000..3f2c248 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/chat/IGroupChat.java @@ -0,0 +1,87 @@ +package org.mercury_im.messenger.entity.chat; + +import org.mercury_im.messenger.entity.Account; +import org.mercury_im.messenger.entity.contact.Peer; + +import java.util.Set; +import java.util.UUID; + +public class IGroupChat implements GroupChat { + + private UUID id; + private Account account; + private String roomAddress; + private String roomName; + protected ChatPreferences preferences; + protected Set participants; + + public IGroupChat() { + this(UUID.randomUUID()); + } + + public IGroupChat(UUID id) { + this.id = id; + } + + @Override + public Set getParticipants() { + return participants; + } + + @Override + public void setParticipants(Set participants) { + this.participants = participants; + } + + @Override + public String getRoomAddress() { + return roomAddress; + } + + @Override + public void setRoomAddress(String roomAddress) { + this.roomAddress = roomAddress; + } + + @Override + public String getRoomName() { + return roomName; + } + + @Override + public void setRoomName(String roomName) { + this.roomName = roomName; + } + + @Override + public UUID getId() { + return id; + } + + @Override + public void setId(UUID id) { + this.id = id; + } + + @Override + public Account getAccount() { + return account; + } + + @Override + public void setAccount(Account account) { + this.account = account; + } + + + @Override + public ChatPreferences getChatPreferences() { + return preferences; + } + + @Override + public void setChatPreferences(ChatPreferences chatPreferences) { + this.preferences = chatPreferences; + } + +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/chat/INotificationPreferences.java b/entity/src/main/java/org/mercury_im/messenger/entity/chat/INotificationPreferences.java new file mode 100644 index 0000000..dfffcd2 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/chat/INotificationPreferences.java @@ -0,0 +1,27 @@ +package org.mercury_im.messenger.entity.chat; + +public class INotificationPreferences implements ChatPreferences.NotificationPreferences { + + protected boolean notifyOnMessage; + protected boolean notifyOnMention; + + @Override + public boolean isNotifyOnMessage() { + return notifyOnMessage; + } + + @Override + public void setNotifyOnMessage(boolean notify) { + this.notifyOnMessage = notify; + } + + @Override + public boolean isNotifyOnMention() { + return notifyOnMention; + } + + @Override + public void setNotifyOnMention(boolean notify) { + this.notifyOnMention = notify; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/contact/IPeer.java b/entity/src/main/java/org/mercury_im/messenger/entity/contact/IPeer.java new file mode 100644 index 0000000..a1e282a --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/contact/IPeer.java @@ -0,0 +1,100 @@ +package org.mercury_im.messenger.entity.contact; + +import org.mercury_im.messenger.entity.Account; + +import java.util.UUID; + +public class IPeer implements Peer { + + protected UUID id; + protected Account account; + protected String address; + protected String name; + protected SubscriptionDirection subscriptionDirection; + protected boolean pending; + protected boolean approved; + + public IPeer() { + this(UUID.randomUUID()); + } + + public IPeer(UUID id) { + this.id = id; + } + + @Override + public UUID getId() { + return id; + } + + @Override + public void setId(UUID id) { + this.id = id; + } + + @Override + public Account getAccount() { + return account; + } + + @Override + public void setAccount(Account account) { + this.account = account; + } + + @Override + public String getAddress() { + return address; + } + + @Override + public void setAddress(String address) { + this.address = address; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public SubscriptionDirection getSubscriptionDirection() { + return subscriptionDirection; + } + + @Override + public void setSubscriptionDirection(SubscriptionDirection mode) { + this.subscriptionDirection = mode; + } + + @Override + public boolean isSubscriptionPending() { + return pending; + } + + @Override + public void setSubscriptionPending(boolean pending) { + this.pending = pending; + } + + @Override + public boolean isSubscriptionApproved() { + return approved; + } + + @Override + public void setSubscriptionApproved(boolean approved) { + this.approved = approved; + } + + @Override + public boolean isContact() { + return subscriptionDirection != SubscriptionDirection.none + && subscriptionDirection != SubscriptionDirection.from; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/contact/Peer.java b/entity/src/main/java/org/mercury_im/messenger/entity/contact/Peer.java new file mode 100644 index 0000000..3a33a44 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/contact/Peer.java @@ -0,0 +1,48 @@ +package org.mercury_im.messenger.entity.contact; + +import org.mercury_im.messenger.entity.Account; + +import java.util.UUID; + +/** + * Defines a user on the network (eg. a contact, chat partner, group chat member etc). + * Basically anyone that may send you a message is a Peer. + * + * An implementation can be found as {@link IPeer}. + */ +public interface Peer { + + UUID getId(); + + void setId(UUID id); + + Account getAccount(); + + void setAccount(Account account); + + String getAddress(); + + void setAddress(String address); + + String getName(); + + void setName(String name); + + SubscriptionDirection getSubscriptionDirection(); + + void setSubscriptionDirection(SubscriptionDirection mode); + + boolean isSubscriptionPending(); + + void setSubscriptionPending(boolean pending); + + boolean isSubscriptionApproved(); + + void setSubscriptionApproved(boolean approved); + + boolean isContact(); + + default String getDisplayName() { + return getName() != null ? getName() : getAddress(); + } +} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/enums/SubscriptionDirection.java b/entity/src/main/java/org/mercury_im/messenger/entity/contact/SubscriptionDirection.java similarity index 63% rename from persistence/src/main/java/org/mercury_im/messenger/persistence/enums/SubscriptionDirection.java rename to entity/src/main/java/org/mercury_im/messenger/entity/contact/SubscriptionDirection.java index b640987..90a3671 100644 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/enums/SubscriptionDirection.java +++ b/entity/src/main/java/org/mercury_im/messenger/entity/contact/SubscriptionDirection.java @@ -1,4 +1,4 @@ -package org.mercury_im.messenger.persistence.enums; +package org.mercury_im.messenger.entity.contact; public enum SubscriptionDirection { none, diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/event/ITypingEvent.java b/entity/src/main/java/org/mercury_im/messenger/entity/event/ITypingEvent.java new file mode 100644 index 0000000..bc5d4eb --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/event/ITypingEvent.java @@ -0,0 +1,20 @@ +package org.mercury_im.messenger.entity.event; + +import org.mercury_im.messenger.entity.contact.Peer; + +import java.util.Map; + +public class ITypingEvent implements TypingEvent { + + public Map typingStates; + + @Override + public Map getTypingStates() { + return typingStates; + } + + @Override + public void setTypingStates(Map typingStates) { + this.typingStates = typingStates; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/event/TypingEvent.java b/entity/src/main/java/org/mercury_im/messenger/entity/event/TypingEvent.java new file mode 100644 index 0000000..c728929 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/event/TypingEvent.java @@ -0,0 +1,17 @@ +package org.mercury_im.messenger.entity.event; + +import org.mercury_im.messenger.entity.contact.Peer; + +import java.util.Map; + +/** + * Event of someone typing in a chat. + * + * An implementation can be found as {@link ITypingEvent}. + */ +public interface TypingEvent { + + Map getTypingStates(); + + void setTypingStates(Map typingStates); +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/event/TypingState.java b/entity/src/main/java/org/mercury_im/messenger/entity/event/TypingState.java new file mode 100644 index 0000000..f27c125 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/event/TypingState.java @@ -0,0 +1,24 @@ +package org.mercury_im.messenger.entity.event; + +public enum TypingState { + /** + * The peer is typing. + */ + typing, + + /** + * The peer was typing and is now pausing for a short amount of time. + */ + pause, + + /** + * The peer is deleting from their input field. + */ + deleting, + + /** + * The peer stopped typing completely. + * TODO: Makes sense? + */ + stop +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/IMessage.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/IMessage.java new file mode 100644 index 0000000..313c11f --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/IMessage.java @@ -0,0 +1,105 @@ +package org.mercury_im.messenger.entity.message; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +public class IMessage implements Message { + + protected UUID id; + protected String sender; + protected String recipient; + protected Date timestamp; + protected List payloads; + protected MessageDeliveryState deliveryState; + protected MessageMetadata metadata; + protected MessageDirection direction; + + public IMessage() { + this(UUID.randomUUID()); + } + + public IMessage(UUID id) { + this.id = id; + } + + @Override + public UUID getId() { + return id; + } + + @Override + public void setId(UUID id) { + this.id = id; + } + + @Override + public String getSender() { + return sender; + } + + @Override + public void setSender(String address) { + this.sender = address; + } + + @Override + public String getRecipient() { + return recipient; + } + + @Override + public void setRecipient(String recipient) { + this.recipient = recipient; + } + + @Override + public Date getTimestamp() { + return timestamp; + } + + @Override + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + @Override + public MessageDirection getDirection() { + return direction; + } + + @Override + public void setDirection(MessageDirection direction) { + this.direction = direction; + } + + @Override + public List getMessagePayloads() { + return payloads; + } + + @Override + public void setMessagePayloads(List list) { + this.payloads = list; + } + + @Override + public MessageDeliveryState getDeliveryState() { + return deliveryState; + } + + @Override + public void setDeliveryState(MessageDeliveryState deliveryState) { + this.deliveryState = deliveryState; + } + + @Override + public MessageMetadata getMetadata() { + return metadata; + } + + @Override + public void setMetadata(MessageMetadata metadata) { + this.metadata = metadata; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/IMessageMetadata.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/IMessageMetadata.java new file mode 100644 index 0000000..931cd06 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/IMessageMetadata.java @@ -0,0 +1,41 @@ +package org.mercury_im.messenger.entity.message; + +public class IMessageMetadata implements MessageMetadata { + + private long id; + private String legacyStanzaId; + private String originId; + private String stanzaId; + + public void setId(long id) { + this.id = id; + } + + public void setLegacyStanzaId(String legacyStanzaId) { + this.legacyStanzaId = legacyStanzaId; + } + + public void setOriginId(String originId) { + this.originId = originId; + } + + public void setStanzaId(String stanzaId) { + this.stanzaId = stanzaId; + } + + public long getId() { + return id; + } + + public String getLegacyStanzaId() { + return legacyStanzaId; + } + + public String getOriginId() { + return originId; + } + + public String getStanzaId() { + return stanzaId; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/IPayloadContainer.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/IPayloadContainer.java new file mode 100644 index 0000000..d5337bd --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/IPayloadContainer.java @@ -0,0 +1,31 @@ +package org.mercury_im.messenger.entity.message; + +import org.mercury_im.messenger.entity.message.content.Payload; + +import java.util.List; + +public class IPayloadContainer implements PayloadContainer { + + protected long id; + protected List contents; + + @Override + public long getId() { + return id; + } + + @Override + public void setId(long id) { + this.id = id; + } + + @Override + public List getMessageContents() { + return contents; + } + + @Override + public void setMessageContents(List payloads) { + this.contents = payloads; + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/Message.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/Message.java new file mode 100644 index 0000000..affed75 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/Message.java @@ -0,0 +1,44 @@ +package org.mercury_im.messenger.entity.message; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +public interface Message { + + UUID getId(); + + void setId(UUID id); + + String getSender(); + + void setSender(String address); + + String getRecipient(); + + void setRecipient(String recipient); + + Date getTimestamp(); + + void setTimestamp(Date timestamp); + + MessageDirection getDirection(); + + void setDirection(MessageDirection direction); + + default boolean isIncoming() { + return getDirection() == MessageDirection.incoming; + } + + List getMessagePayloads(); + + void setMessagePayloads(List payloadContainers); + + MessageDeliveryState getDeliveryState(); + + void setDeliveryState(MessageDeliveryState deliveryState); + + MessageMetadata getMetadata(); + + void setMetadata(MessageMetadata metadata); +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/MessageDeliveryState.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/MessageDeliveryState.java new file mode 100644 index 0000000..2485638 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/MessageDeliveryState.java @@ -0,0 +1,10 @@ +package org.mercury_im.messenger.entity.message; + +public enum MessageDeliveryState { + pending_delivery, + delivered_to_server, + delivered_to_peer, + read, + delivery_error, + ; +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/MessageDirection.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/MessageDirection.java new file mode 100644 index 0000000..1315138 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/MessageDirection.java @@ -0,0 +1,6 @@ +package org.mercury_im.messenger.entity.message; + +public enum MessageDirection { + incoming, + outgoing +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/MessageMetadata.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/MessageMetadata.java new file mode 100644 index 0000000..03e7ac7 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/MessageMetadata.java @@ -0,0 +1,9 @@ +package org.mercury_im.messenger.entity.message; + +/** + * Interface to allow additional, protocol specific metadata to be attached to the message. + * In case of XMPP this might be origin/stanza id, encryption information etc. + */ +public interface MessageMetadata { + +} \ No newline at end of file diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/PayloadContainer.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/PayloadContainer.java new file mode 100644 index 0000000..1e31c04 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/PayloadContainer.java @@ -0,0 +1,26 @@ +package org.mercury_im.messenger.entity.message; + +import org.mercury_im.messenger.entity.message.content.Payload; + +import java.util.List; + +/** + * Defines a certain set of {@link Payload Payloads} of a message. + * A {@link PayloadContainer} can either be a plaintext container or an encrypted container and contains + * {@link Payload Payloads}. + * + * A message may contain encrypted and unencrypted payloads. Those could then be represented by + * two different {@link PayloadContainer PayloadContainers}. + */ +public interface PayloadContainer { + + long getId(); + + void setId(long id); + + List getMessageContents(); + + void setMessageContents(List payloads); + + +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/content/Payload.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/content/Payload.java new file mode 100644 index 0000000..f84a8f7 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/content/Payload.java @@ -0,0 +1,25 @@ +package org.mercury_im.messenger.entity.message.content; + +/** + * Interface describing contents of a message. + * + * A message consists of general information like sender, recipient etc. + * Additionally a message contains at least one payload container, eg. the set of unencrypted + * message contents, or the set of encrypted contents. + * + * Inside those payloads, message contents exist. + */ +public interface Payload { + + long getId(); + + void setId(long id); + + interface Body extends Payload { + + String getBody(); + + void setBody(String body); + + } +} diff --git a/entity/src/main/java/org/mercury_im/messenger/entity/message/content/TextPayload.java b/entity/src/main/java/org/mercury_im/messenger/entity/message/content/TextPayload.java new file mode 100644 index 0000000..0500d18 --- /dev/null +++ b/entity/src/main/java/org/mercury_im/messenger/entity/message/content/TextPayload.java @@ -0,0 +1,27 @@ +package org.mercury_im.messenger.entity.message.content; + +public class TextPayload implements Payload.Body { + + private long id; + private String body; + + @Override + public long getId() { + return id; + } + + @Override + public void setId(long id) { + this.id = id; + } + + @Override + public String getBody() { + return body; + } + + @Override + public void setBody(String body) { + this.body = body; + } +} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/converter/EntityBareJidConverter.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/converter/EntityBareJidConverter.java deleted file mode 100644 index 77c66d5..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/converter/EntityBareJidConverter.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.mercury_im.messenger.persistence.converter; - -import org.jxmpp.jid.EntityBareJid; -import org.jxmpp.jid.impl.JidCreate; - -import io.requery.Converter; - -public class EntityBareJidConverter implements Converter { - @Override - public Class getMappedType() { - return EntityBareJid.class; - } - - @Override - public Class getPersistedType() { - return String.class; - } - - @Override - public Integer getPersistedSize() { - return null; - } - - @Override - public String convertToPersisted(EntityBareJid jid) { - return jid == null ? null : jid.asUnescapedString(); - } - - @Override - public EntityBareJid convertToMapped(Class aClass, String string) { - return string == null ? null : JidCreate.entityBareFromOrThrowUnchecked(string); - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/di/RequeryModule.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/di/RequeryModule.java deleted file mode 100644 index 6ba1219..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/di/RequeryModule.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.mercury_im.messenger.persistence.di; - -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.RosterRepository; -import org.mercury_im.messenger.thread_utils.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 -public class RequeryModule { - - @Provides - @Singleton - public static AccountRepository provideAccountRepository(ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler) { - return new AccountRepository(data, ioScheduler, uiScheduler); - } - - @Provides - @Singleton - public static ChatRepository provideChatRepository(ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler) { - return new ChatRepository(data, ioScheduler, uiScheduler); - } - - @Provides - @Singleton - public static EntityCapsRepository provideCapsRepository(ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler) { - return new EntityCapsRepository(data, ioScheduler, uiScheduler); - } - - @Provides - @Singleton - public static RosterRepository provideRosterRepository(ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler ioScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler uiScheduler) { - return new RosterRepository(data, ioScheduler, uiScheduler); - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractChatModel.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractChatModel.java deleted file mode 100644 index 068618a..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractChatModel.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.mercury_im.messenger.persistence.entity; - -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; - -@Entity -@Table(name = "chats") -public abstract class AbstractChatModel implements Persistable { - - @Key @Generated - long id; - - @OneToOne - @ForeignKey - EntityModel peer; - - boolean displayed; -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractContactModel.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractContactModel.java deleted file mode 100644 index 17b36b3..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractContactModel.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.mercury_im.messenger.persistence.entity; - -import org.mercury_im.messenger.persistence.converter.SubscriptionDirectionConverter; -import org.mercury_im.messenger.persistence.enums.SubscriptionDirection; - -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; - -@Entity -@Table(name = "contacts") -public abstract class AbstractContactModel implements Persistable { - - @Key @Generated - long id; - - @OneToOne - @ForeignKey(referencedColumn = "id") - EntityModel entity; - - String rostername; - - @Convert(SubscriptionDirectionConverter.class) - SubscriptionDirection sub_direction; - - boolean sub_pending; - - boolean sub_approved; - - @Override - public String toString() { - return "Contact[" + id + ", " + - rostername + ", " + - entity + ", " + - sub_direction + ", " + - (sub_pending ? "pending" : "not pending") + ", " + - (sub_approved ? "approved" : "not approved") + "]"; - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractEntityModel.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractEntityModel.java deleted file mode 100644 index 44c25fc..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractEntityModel.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.mercury_im.messenger.persistence.entity; - -import org.jxmpp.jid.EntityBareJid; -import org.mercury_im.messenger.persistence.converter.EntityBareJidConverter; - -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; - -@Entity -@Table(name = "entities") -public abstract class AbstractEntityModel implements Persistable { - - @Key @Generated - long id; - - @ManyToOne - AccountModel account; - - @Convert(EntityBareJidConverter.class) - @Column(nullable = false) - EntityBareJid jid; - - @Override - public String toString() { - return "Entity[" + id + ", " + - jid + ", " + - account + "]"; - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractLastChatMessageRelation.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractLastChatMessageRelation.java deleted file mode 100644 index b7f8f65..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractLastChatMessageRelation.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.mercury_im.messenger.persistence.entity; - -import io.requery.Entity; -import io.requery.ForeignKey; -import io.requery.Key; -import io.requery.OneToOne; -import io.requery.Persistable; -import io.requery.Table; - -@Entity -@Table(name = "last_messages") -public abstract class AbstractLastChatMessageRelation implements Persistable { - - @Key - @OneToOne - @ForeignKey - ChatModel chat; - - @OneToOne - @ForeignKey - MessageModel message; -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractLastReadChatMessageRelation.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractLastReadChatMessageRelation.java deleted file mode 100644 index 5fb86d4..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractLastReadChatMessageRelation.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.mercury_im.messenger.persistence.entity; - -import io.requery.Entity; -import io.requery.ForeignKey; -import io.requery.Key; -import io.requery.OneToOne; -import io.requery.Persistable; -import io.requery.Table; - -@Entity -@Table(name = "last_read_messages") -public abstract class AbstractLastReadChatMessageRelation implements Persistable { - - @Key - @OneToOne - @ForeignKey - ChatModel chat; - - @OneToOne - @ForeignKey - MessageModel message; -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractMessageModel.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractMessageModel.java deleted file mode 100644 index 144024f..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/entity/AbstractMessageModel.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.mercury_im.messenger.persistence.entity; - -import org.jxmpp.jid.EntityBareJid; -import org.mercury_im.messenger.persistence.converter.EntityBareJidConverter; - -import java.util.Date; - -import io.requery.Column; -import io.requery.Convert; -import io.requery.Entity; -import io.requery.ForeignKey; -import io.requery.Generated; -import io.requery.Key; -import io.requery.ManyToOne; -import io.requery.Persistable; -import io.requery.Table; - -@Entity -@Table(name = "messages") -public abstract class AbstractMessageModel implements Persistable { - - @Key @Generated - long id; - - @ForeignKey(referencedColumn = "id") - @ManyToOne - ChatModel chat; - - String body; - - @Column(name = "\"timestamp\"", nullable = false) - Date timestamp; - - @Convert(EntityBareJidConverter.class) - @Column(nullable = false) - EntityBareJid sender; - - @Convert(EntityBareJidConverter.class) - @Column(nullable = false) - EntityBareJid recipient; - - boolean incoming; - - String thread; - - @Column(nullable = false) - String legacyId; - - String originId; - - String stanzaId; - - @Convert(EntityBareJidConverter.class) - EntityBareJid stanzaIdBy; -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/AbstractRepository.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/AbstractRepository.java deleted file mode 100644 index 1e764b5..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/AbstractRepository.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.mercury_im.messenger.persistence.repository; - -import org.mercury_im.messenger.thread_utils.ThreadUtils; - -import javax.inject.Named; - -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.Scheduler; -import io.reactivex.Single; -import io.requery.Persistable; -import io.requery.reactivex.ReactiveEntityStore; -import io.requery.reactivex.ReactiveResult; - -public abstract class AbstractRepository extends RequeryRepository { - - - private final Class modelType; - - protected AbstractRepository(Class modelType, - ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler) { - super(data, subscriberScheduler, observerScheduler); - this.modelType = modelType; - } - - // CRUD - - public Single insert(E model) { - return data().insert(model) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Single> insert(Iterable models) { - return data().insert(models) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Single upsert(E model) { - return data().upsert(model) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Single> upsert(Iterable models) { - return data().upsert(models) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Single update(E model) { - return data().update(model) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Single> update(Iterable models) { - return data().update(models) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable> getAll() { - return data().select(modelType) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Completable delete(E model) { - return data().delete(model) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Completable delete(Iterable models) { - return data().delete(models) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Single deleteAll() { - return data().delete(modelType) - .get().single(); - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/AccountRepository.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/AccountRepository.java deleted file mode 100644 index fbb2d3e..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/AccountRepository.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.mercury_im.messenger.persistence.repository; - -import org.jxmpp.jid.EntityBareJid; -import org.mercury_im.messenger.persistence.entity.AccountModel; -import org.mercury_im.messenger.thread_utils.ThreadUtils; - -import javax.inject.Inject; -import javax.inject.Named; - -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.Scheduler; -import io.requery.Persistable; -import io.requery.reactivex.ReactiveEntityStore; -import io.requery.reactivex.ReactiveResult; - -public class AccountRepository extends AbstractRepository { - - @Inject - public AccountRepository(ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler) { - super(AccountModel.class, data, subscriberScheduler, observerScheduler); - } - - public Observable> getAccountByJid(EntityBareJid jid) { - return data().select(AccountModel.class) - .where(AccountModel.JID.eq(jid)) - .get() - .observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable> getAccount(long accountId) { - return data().select(AccountModel.class).where(AccountModel.ID.eq(accountId)) - .get().observableResult() - .subscribeOn(subscriberScheduler()).observeOn(observerScheduler()); - } - - public Completable delete(long accountId) { - return data().delete(AccountModel.class) - .where(AccountModel.ID.eq(accountId)) - .get() - .single() - .ignoreElement() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/ChatRepository.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/ChatRepository.java deleted file mode 100644 index 943846b..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/ChatRepository.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.mercury_im.messenger.persistence.repository; - -import org.jxmpp.jid.EntityBareJid; -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.util.ChatAndPossiblyContact; -import org.mercury_im.messenger.thread_utils.ThreadUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.inject.Inject; -import javax.inject.Named; - -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.Scheduler; -import io.reactivex.functions.Function; -import io.requery.Persistable; -import io.requery.query.Tuple; -import io.requery.reactivex.ReactiveEntityStore; -import io.requery.reactivex.ReactiveResult; - -public class ChatRepository extends AbstractRepository { - - private final Logger LOGGER = Logger.getLogger(ChatRepository.class.getName()); - - @Inject - public ChatRepository(ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler) { - super(ChatModel.class, data, subscriberScheduler, observerScheduler); - } - - public Observable> getChatWith(EntityModel entityModel) { - return getChatWith(entityModel.getAccount().getId(), entityModel.getJid()); - } - - public Observable> getChatWith(long accountId, EntityBareJid jid) { - return data().select(ChatModel.class).join(EntityModel.class).on(ChatModel.PEER_ID.eq(EntityModel.ID)) - .where(EntityModel.ACCOUNT_ID.eq(accountId).and(EntityModel.JID.eq(jid))) - .get().observableResult() - .doOnError(error -> LOGGER.log(Level.WARNING, "An error occurred while getting chat", error)) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable> getVisibleChats() { - return data().select(ChatModel.class).where(ChatModel.DISPLAYED.eq(true)) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable> getChatsAndContacts() { - return Observable.fromCallable(() -> { - List chats = data().select(ChatModel.class) - .from(ChatModel.class).leftJoin(ContactModel.class).on(ChatModel.PEER_ID.eq(ContactModel.ENTITY_ID)) - .get().toList(); - List chatsAndContacts = new ArrayList<>(chats.size()); - for (ChatModel chatModel : chats) { - ContactModel contactModel = data().select(ContactModel.class).from(ContactModel.class).where(ContactModel.ENTITY_ID.eq(chatModel.getPeer().getId())) - .get().first(); - chatsAndContacts.add(new ChatAndPossiblyContact(chatModel, contactModel)); - } - return chatsAndContacts; - }) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable getChatAndContact(long accountId, EntityBareJid jid) { - - return Observable.fromCallable(() -> { - ChatModel chat = data().select(ChatModel.class) - .from(ChatModel.class).join(EntityModel.class).on(ChatModel.PEER_ID.eq(EntityModel.ID)) - .leftJoin(ContactModel.class).on(EntityModel.ID.eq(ContactModel.ENTITY_ID)) - .where(EntityModel.ACCOUNT_ID.eq(accountId).and(EntityModel.JID.eq(jid))) - .get().firstOrNull(); - if (chat == null) { - return null; - } - - ContactModel contactModel = data().select(ContactModel.class).from(ContactModel.class) - .where(ContactModel.ENTITY_ID.eq(chat.getPeer().getId())) - .get().firstOrNull(); - return new ChatAndPossiblyContact(chat, contactModel); - }) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/EntityCapsRepository.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/EntityCapsRepository.java deleted file mode 100644 index 55db16a..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/EntityCapsRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.mercury_im.messenger.persistence.repository; - -import org.mercury_im.messenger.persistence.entity.EntityCapsModel; -import org.mercury_im.messenger.thread_utils.ThreadUtils; - -import javax.inject.Inject; -import javax.inject.Named; - -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.Scheduler; -import io.requery.Persistable; -import io.requery.reactivex.ReactiveEntityStore; -import io.requery.reactivex.ReactiveResult; - -public class EntityCapsRepository extends AbstractRepository { - - @Inject - public EntityCapsRepository(ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler) { - super(EntityCapsModel.class, data, subscriberScheduler, observerScheduler); - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/MessageRepository.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/MessageRepository.java deleted file mode 100644 index 1de183e..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/MessageRepository.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.mercury_im.messenger.persistence.repository; - -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.thread_utils.ThreadUtils; - -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; - -import io.reactivex.Observable; -import io.reactivex.Scheduler; -import io.reactivex.Single; -import io.requery.Persistable; -import io.requery.reactivex.ReactiveEntityStore; -import io.requery.reactivex.ReactiveResult; - -public class MessageRepository extends AbstractRepository { - - @Inject - public MessageRepository(ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler) { - super(MessageModel.class, data, subscriberScheduler, observerScheduler); - } - - public Observable> getAllMessagesOfChat(ChatModel chat) { - return getAllMessagesOfChat(chat.getId()); - } - - public Observable> getAllMessagesOfChat(long chatId) { - return data().select(MessageModel.class).where(MessageModel.CHAT_ID.eq(chatId)) - .orderBy(MessageModel.TIMESTAMP.asc()) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable> getAllMessagesOfEntity(EntityModel entity) { - return getAllMessagesOfEntity(entity.getId()); - } - - public Observable> getAllMessagesOfEntity(long entityId) { - return data().select(MessageModel.class).from(MessageModel.class) - .join(ChatModel.class).on(MessageModel.CHAT_ID.eq(ChatModel.ID)) - .where(ChatModel.PEER_ID.eq(entityId)) - .orderBy(MessageModel.TIMESTAMP.asc()) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable> getAllMessagesOfContact(ContactModel contact) { - return getAllMessagesOfContact(contact.getId()); - } - - public Observable> getAllMessagesOfContact(long contactId) { - return data().select(MessageModel.class).join(ChatModel.class).on(MessageModel.CHAT_ID.eq(ChatModel.ID)) - .join(EntityModel.class).on(ChatModel.PEER_ID.eq(EntityModel.ID)) - .join(ContactModel.class).on(EntityModel.ID.eq(ContactModel.ENTITY_ID)) - .orderBy(MessageModel.TIMESTAMP.asc()) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/RosterRepository.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/RosterRepository.java deleted file mode 100644 index ea050eb..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/repository/RosterRepository.java +++ /dev/null @@ -1,202 +0,0 @@ -package org.mercury_im.messenger.persistence.repository; - -import org.jxmpp.jid.EntityBareJid; -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.thread_utils.ThreadUtils; - -import javax.inject.Inject; -import javax.inject.Named; - -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.Scheduler; -import io.reactivex.Single; -import io.requery.Persistable; -import io.requery.reactivex.ReactiveEntityStore; -import io.requery.reactivex.ReactiveResult; - -public class RosterRepository extends RequeryRepository { - - @Inject - public RosterRepository(ReactiveEntityStore data, - @Named(value = ThreadUtils.SCHEDULER_IO) Scheduler subscriberScheduler, - @Named(value = ThreadUtils.SCHEDULER_UI) Scheduler observerScheduler) { - super(data, subscriberScheduler, observerScheduler); - } - - /* - ContactModel related methods - */ - - public Observable> getAllContactsOfAccount(AccountModel accountModel) { - return getAllContactsOfAccount(accountModel.getId()); - } - - public Observable> getAllContactsOfAccount(long accountId) { - return data().select(ContactModel.class).join(EntityModel.class).on(ContactModel.ENTITY_ID.eq(EntityModel.ID)) - .where(EntityModel.ACCOUNT_ID.eq(accountId)) - .get().observableResult() - .subscribeOn(subscriberScheduler()).observeOn(observerScheduler()); - } - - public Single upsertContact(ContactModel contact) { - return data().upsert(contact).subscribeOn(subscriberScheduler()).observeOn(observerScheduler()); - } - - public Completable deleteContact(ContactModel contact) { - return data().delete(contact).subscribeOn(subscriberScheduler()).observeOn(observerScheduler()); - } - - public Completable deleteContact(long accountId, EntityBareJid jid) { - return data().delete(ContactModel.class).from(ContactModel.class) - .join(EntityModel.class).on(ContactModel.ENTITY_ID.eq(EntityModel.ID)) - .where(EntityModel.ACCOUNT_ID.eq(accountId).and(EntityModel.JID.eq(jid))) - .get().single().ignoreElement() - .subscribeOn(subscriberScheduler()).observeOn(observerScheduler()); - } - - public Single deleteAllContactsOfAccount(AccountModel account) { - return deleteAllContactsOfAccount(account.getId()); - } - - public Single deleteAllContactsOfAccount(long accountId) { - return data().delete(ContactModel.class).from(ContactModel.class) - .join(EntityModel.class).on(ContactModel.ENTITY_ID.eq(EntityModel.ID)) - .where(EntityModel.ACCOUNT_ID.eq(accountId)) - .get().single() - .subscribeOn(subscriberScheduler()).observeOn(observerScheduler()); - } - - - /* - EntityModel related methods - */ - - public Observable> getAllEntitiesOfAccount(AccountModel account) { - return getAllEntitiesOfAccount(account.getId()); - } - - public Observable> getAllEntitiesOfAccount(long accountId) { - return data().select(EntityModel.class).where(EntityModel.ACCOUNT_ID.eq(accountId)) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable> getEntityById(long entityId) { - return data().select(EntityModel.class).where(EntityModel.ID.eq(entityId)) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable> getEntityByJid(AccountModel account, EntityBareJid jid) { - return getEntityByJid(account.getId(), jid); - } - - public Observable> getEntityByJid(long accountId, EntityBareJid jid) { - return data().select(EntityModel.class) - .where(EntityModel.ACCOUNT_ID.eq(accountId).and(EntityModel.JID.eq(jid))) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Single getOrCreateEntity(long accountId, EntityBareJid jid) { - return Single.fromCallable(() -> { - AccountModel account = data().select(AccountModel.class).where(AccountModel.ID.eq(accountId)) - .get().first(); - return getOrCreateEntity(account, jid).blockingGet(); - }) - .observeOn(observerScheduler()) - .subscribeOn(subscriberScheduler()); - } - - public Single getOrCreateEntity(AccountModel account, EntityBareJid jid) { - return Single.fromCallable(() -> { - EntityModel entity = data().select(EntityModel.class) - .where(EntityModel.ACCOUNT_ID.eq(account.getId()).and(EntityModel.JID.eq(jid))) - .get().firstOrNull(); - if (entity == null) { - entity = new EntityModel(); - entity.setAccount(account); - entity.setJid(jid); - entity = data().insert(entity).blockingGet(); - } - return entity; - }) - .observeOn(observerScheduler()) - .subscribeOn(subscriberScheduler()); - - } - - public Single upsertEntity(EntityModel entity) { - return data().upsert(entity) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Completable deleteEntity(EntityModel entity) { - return data().delete(entity) - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - /* - RosterVersion related methods - */ - - public Single updateRosterVersion(long accountId, String rosterVer) { - return data().update(AccountModel.class).set(AccountModel.ROSTER_VERSION, rosterVer) - .where(AccountModel.ID.eq(accountId)) - .get().single() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable getRosterVersion(AccountModel account) { - return getRosterVersion(account.getId()); - } - - public Observable getRosterVersion(long accountId) { - return data().select(AccountModel.class).where(AccountModel.ID.eq(accountId)) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()) - .map(accountModels -> { - AccountModel accountModel = accountModels.firstOrNull(); - if (accountModel == null || accountModel.getRosterVersion() == null) { - return ""; - } - return accountModel.getRosterVersion(); - }); - } - - public Single updateRosterVersion(AccountModel account, String rosterVersion) { - account.setRosterVersion(rosterVersion); - return data().upsert(account) - .subscribeOn(subscriberScheduler()).observeOn(observerScheduler()); - } - - public Observable> getContact(AccountModel account, EntityBareJid jid) { - return getContact(account.getId(), jid); - } - - public Observable> getContact(long accountId, EntityBareJid jid) { - return data().select(ContactModel.class).from(ContactModel.class) - .join(EntityModel.class).on(ContactModel.ENTITY_ID.eq(EntityModel.ID)) - .where(EntityModel.ACCOUNT_ID.eq(accountId).and(EntityModel.JID.eq(jid))) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } - - public Observable> getAllContacts() { - return data().select(ContactModel.class) - .get().observableResult() - .subscribeOn(subscriberScheduler()) - .observeOn(observerScheduler()); - } -} diff --git a/persistence/src/main/java/org/mercury_im/messenger/persistence/util/ChatAndPossiblyContact.java b/persistence/src/main/java/org/mercury_im/messenger/persistence/util/ChatAndPossiblyContact.java deleted file mode 100644 index 1b5d13b..0000000 --- a/persistence/src/main/java/org/mercury_im/messenger/persistence/util/ChatAndPossiblyContact.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.mercury_im.messenger.persistence.util; - -import org.mercury_im.messenger.persistence.entity.ChatModel; -import org.mercury_im.messenger.persistence.entity.ContactModel; - -public class ChatAndPossiblyContact { - - private final ChatModel chat; - private final ContactModel contact; - - public ChatAndPossiblyContact(ChatModel chat, ContactModel contact) { - this.chat = chat; - this.contact = contact; - } - - public ChatModel getChat() { - return chat; - } - - public ContactModel getContact() { - return contact; - } -} diff --git a/settings.gradle b/settings.gradle index 36f15fb..7f2ec34 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,7 @@ -include ':app', ':thread_utils', - ':core', - ':persistence' +include ':entity', + ':data', + ':domain', + ':app' + // ':core-old' includeBuild 'libs/Smack' diff --git a/thread_utils/README.md b/thread_utils/README.md deleted file mode 100644 index 2efbb17..0000000 --- a/thread_utils/README.md +++ /dev/null @@ -1,5 +0,0 @@ -The purpose of this module is to hold the `ThreadUtils` class, -on which both modules `persistence` as well as `core` depend. - -By moving that class into a separate module, we prevent a circular -dependency between `core` and `persistence`. \ No newline at end of file diff --git a/version.gradle b/version.gradle index 25d6a4d..802b20a 100644 --- a/version.gradle +++ b/version.gradle @@ -73,7 +73,7 @@ ext { // Other libraries // Architecture Components - lifecycleVersion = '2.2.0-alpha05' + lifecycleVersion = '2.2.0-rc03' pagingVersion = "2.1.0" appCompatVersion = '1.1.0' @@ -86,16 +86,16 @@ ext { rxAndroidVersion = "2.1.1" // Dagger 2 - daggerVersion = '2.24' + daggerVersion = '2.25.4' // Android Support Library supportLibVersion = "28.0.0" // Butter Knife - butterKnifeVersion = '10.2.0' + butterKnifeVersion = '10.2.1' // JUnit - junitVersion = "4.12" + junitVersion = '4.13' andxTestJunitVersion = "1.1.1" // androidx.test:runner