experimental MAM and improved rx repos

This commit is contained in:
Paul Schaub 2019-09-08 04:47:59 +02:00
parent 93bcda8480
commit 939ce748ef
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
41 changed files with 657 additions and 265 deletions

View file

@ -8,11 +8,9 @@ import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;
import org.mercury_im.messenger.MercuryImApplication;

View file

@ -126,9 +126,9 @@ public class ChatActivity extends AppCompatActivity
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_debug:
chatRepository.getOrCreateChatWith(accountId, JidCreate.entityBareFromOrThrowUnchecked("alice@wonderland.lit"))
Log.d("CHATACTIVITY", "Fetch MAM messages!");
chatViewModel.requestMamMessages()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
break;

View file

@ -6,8 +6,11 @@ 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.model.ChatModel;
import org.mercury_im.messenger.persistence.model.ContactModel;
import org.mercury_im.messenger.persistence.model.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;
@ -15,9 +18,10 @@ import java.util.List;
import javax.inject.Inject;
import io.reactivex.Scheduler;
import io.reactivex.Completable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
@ -31,12 +35,19 @@ public class ChatViewModel extends ViewModel {
@Inject
RosterRepository rosterRepository;
@Inject
ChatRepository chatRepository;
@Inject
ConnectionCenter connectionCenter;
private long accountId;
private EntityBareJid jid;
private MutableLiveData<ContactModel> contact = new MutableLiveData<>();
private MutableLiveData<List<MessageModel>> messages = new MutableLiveData<>();
private MutableLiveData<String> contactDisplayName = new MutableLiveData<>();
private MutableLiveData<ChatModel> chat = new MutableLiveData<>();
public ChatViewModel() {
super();
@ -66,6 +77,12 @@ public class ChatViewModel extends ViewModel {
.observeOn(AndroidSchedulers.mainThread())
.subscribe((Consumer<List<MessageModel>>)
messages -> ChatViewModel.this.messages.setValue(messages)));
disposable.add(chatRepository.getOrCreateChatWith(accountId, jid)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((Consumer<ChatModel>)
chatModel -> this.chat.setValue(chatModel)));
}
@Override
@ -101,4 +118,18 @@ public class ChatViewModel extends ViewModel {
messages.setValue(o);
}));
}
public Completable requestMamMessages() {
return Completable.fromAction(new Action() {
@Override
public void run() throws Exception {
ChatModel chatModel = ChatViewModel.this.chat.getValue();
if (chatModel == null) {
return;
}
connectionCenter.requestMamMessagesFor(chatModel);
}
});
}
}

View file

@ -1,6 +1,7 @@
package org.mercury_im.messenger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mercury_im.messenger.core.connection.ConnectionState;
import io.reactivex.observers.TestObserver;

View file

@ -7,12 +7,16 @@ import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smackx.caps.EntityCapsManager;
import org.jivesoftware.smackx.csi.ClientStateIndicationManager;
import org.jivesoftware.smackx.mam.MamManager;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.core.stores.EntityCapsStore;
import org.mercury_im.messenger.core.connection.MercuryConfiguration;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.core.stores.PlainMessageStore;
import org.mercury_im.messenger.core.stores.RosterStore;
import org.mercury_im.messenger.persistence.model.AccountModel;
import org.mercury_im.messenger.persistence.model.ChatModel;
import org.mercury_im.messenger.persistence.model.EntityModel;
import org.mercury_im.messenger.persistence.repository.AccountRepository;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
@ -213,4 +217,23 @@ public class ConnectionCenter {
}
}
}
public void requestMamMessagesFor(ChatModel chat) {
disposable.add(rosterRepository.getEntity(chat.getPeerEntityId())
.subscribe((Consumer<EntityModel>) entity -> {
MercuryConnection connection = connectionMap.get(entity.getAccountId());
if (connection == null) return;
MamManager mamManager = MamManager.getInstanceFor(connection.getConnection());
MamManager.MamQuery query;
//if (chat.getEarliestMamMessageId() == null) {
query = mamManager.queryMostRecentPage(entity.getJid(), 100);
//} else {
//MamManager.MamQueryArgs queryArgs = MamManager.MamQueryArgs.builder()
// .beforeUid()
// .build();
//query = mamManager.queryArchive()
//}
messageStore.onMamResult(entity.getAccountId(), entity.getJid(), query);
}));
}
}

View file

@ -2,12 +2,12 @@ package org.mercury_im.messenger.core.centers;
import javax.inject.Inject;
public class NotificationCenter {
public class MessageCenter {
private final ConnectionCenter connectionCenter;
@Inject
public NotificationCenter(ConnectionCenter connectionCenter) {
public MessageCenter(ConnectionCenter connectionCenter) {
this.connectionCenter = connectionCenter;
}
}

View file

@ -13,6 +13,7 @@ import org.jivesoftware.smackx.carbons.CarbonManager;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.iqversion.VersionManager;
import org.jivesoftware.smackx.mam.MamManager;
import org.jivesoftware.smackx.sid.StableUniqueStanzaIdManager;
import java.io.IOException;
@ -36,6 +37,7 @@ public class MercuryConnection {
protected final CarbonManager carbonManager;
protected final StableUniqueStanzaIdManager stanzaIdManager;
protected final ServiceDiscoveryManager serviceDiscoveryManager;
protected final MamManager mamManager;
BehaviorSubject<ConnectionState> state = BehaviorSubject.createDefault(ConnectionState.DISCONNECTED);
@ -59,6 +61,8 @@ public class MercuryConnection {
VersionManager.setAutoAppendSmackVersion(false);
VersionManager.getInstanceFor(connection).setVersion("Mercury", "0.0.1-stealth", "Android");
serviceDiscoveryManager.setIdentity(new DiscoverInfo.Identity("client", "Mercury", "phone"));
mamManager = MamManager.getInstanceFor(connection);
}
public void connect() {
@ -120,6 +124,8 @@ public class MercuryConnection {
exception.printStackTrace();
});
}
}
@Override

View file

@ -16,8 +16,10 @@ import java.util.logging.Logger;
import javax.inject.Inject;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.observers.DisposableSingleObserver;
import io.reactivex.schedulers.Schedulers;
@ -29,7 +31,7 @@ public class EntityCapsStore implements EntityCapsPersistentCache {
private final Map<String, DiscoverInfo> discoverInfoMap = new HashMap<>();
private final CompositeDisposable disposable = new CompositeDisposable();
private Single<List<EntityCapsModel>> allEntityCaps;
private Observable<List<EntityCapsModel>> allEntityCaps;
@Inject
public EntityCapsStore(EntityCapsRepository repository) {
@ -41,40 +43,30 @@ public class EntityCapsStore implements EntityCapsPersistentCache {
allEntityCaps = entityCapsRepository.getAllEntityCaps();
disposable.add(allEntityCaps.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribeWith(new DisposableSingleObserver<List<? extends EntityCapsModel>>() {
@Override
public void onSuccess(List<? extends EntityCapsModel> entityCapsModels) {
discoverInfoMap.clear();
for (EntityCapsModel c : entityCapsModels) {
DiscoverInfo info;
try {
XmlPullParser parser = PacketParserUtils.getParserFor(new StringReader(c.getXml()));
info = (DiscoverInfo) PacketParserUtils.parseIQ(parser);
discoverInfoMap.put(c.getNodeVer(), info);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error parsing EntityCaps: ", e);
}
.subscribe(entityCapsModels -> {
discoverInfoMap.clear();
for (EntityCapsModel c : entityCapsModels) {
DiscoverInfo info;
try {
XmlPullParser parser = PacketParserUtils.getParserFor(new StringReader(c.getXml()));
info = (DiscoverInfo) PacketParserUtils.parseIQ(parser);
discoverInfoMap.put(c.getNodeVer(), info);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error parsing EntityCaps: ", e);
}
}
@Override
public void onError(Throwable e) {
LOGGER.log(Level.SEVERE, "Error processing Caps update from store.", e);
}
}));
}, throwable -> LOGGER.log(Level.SEVERE, "Error accessing database", throwable)));
}
@Override
public void addDiscoverInfoByNodePersistent(String nodeVer, DiscoverInfo info) {
discoverInfoMap.put(nodeVer, info);
EntityCapsModel model = entityCapsRepository.newEntityCapsModel(nodeVer);
CharSequence xml = info.toXML();
LOGGER.log(Level.INFO, "Persisting entry:" + xml);
String string = xml.toString();
model.setXml(string);
entityCapsRepository.insertOrReplaceEntityCaps(model)
disposable.add(entityCapsRepository.insertOrReplaceEntityCaps(model)
.subscribeOn(Schedulers.io())
.subscribe();
.subscribe());
}
@Override

View file

@ -5,16 +5,21 @@ import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.carbons.CarbonManager;
import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
import org.jivesoftware.smackx.delay.packet.DelayInformation;
import org.jivesoftware.smackx.mam.MamManager;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.persistence.model.MessageModel;
import org.mercury_im.messenger.persistence.repository.MessageRepository;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public class PlainMessageStore {
@ -102,4 +107,29 @@ public class PlainMessageStore {
public void dispose() {
disposable.clear();
}
public void onMamResult(long accountId, EntityBareJid peerJid, MamManager.MamQuery query) {
List<MessageModel> messageModels = new ArrayList<>();
for (Message message : query.getMessages()) {
Date date = new Date();
DelayInformation delay = DelayInformation.from(message);
if (delay != null) {
date = delay.getStamp();
}
MessageModel messageModel = messageRepository.newMessageModel();
messageModel.setAccountId(accountId);
messageModel.setBody(message.getBody());
messageModel.setFrom(message.getFrom().asEntityBareJidOrThrow());
messageModel.setTo(message.getTo().asEntityBareJidOrThrow());
messageModel.setIncoming(peerJid.equals(message.getFrom().asEntityBareJidOrThrow()));
messageModel.setSendDate(date);
messageModels.add(messageModel);
}
disposable.add(
messageRepository.insertMessages(messageModels)
.subscribeOn(Schedulers.io())
.subscribe());
}
}

View file

@ -104,7 +104,7 @@ public class RosterStore implements org.jivesoftware.smack.roster.rosterstore.Ro
LOGGER.log(Level.INFO, "Add entry " + item.toXML().toString());
// Update database
ContactModel contact = toModel(item);
rosterRepository.updateOrInsertContact(contact)
rosterRepository.upsertContact(contact)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
.subscribe();
@ -122,7 +122,7 @@ public class RosterStore implements org.jivesoftware.smack.roster.rosterstore.Ro
// Update database
for (RosterPacket.Item item : items) {
ContactModel model = toModel(item);
rosterRepository.updateOrInsertContact(model)
rosterRepository.upsertContact(model)
.subscribeOn(Schedulers.io())
.subscribe();
}
@ -174,7 +174,6 @@ public class RosterStore implements org.jivesoftware.smack.roster.rosterstore.Ro
public ContactModel toModel(RosterPacket.Item item) {
ContactModel contact = rosterRepository.newContactModel();
contact.setAccountId(accountId);
contact.setRosterName(item.getName());
if (item.getItemType() != null) {

View file

@ -41,15 +41,16 @@ dependencies {
implementation "com.google.dagger:dagger:$daggerVersion"
annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$andxTestRunnerVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$andxTestEspressoVersion"
androidTestImplementation "androidx.test:core:$andxTestCoreVersion"
androidTestImplementation "androidx.test.ext:junit:$andxTestJunitVersion"
// Room
api "androidx.room:room-runtime:$roomVersion"
annotationProcessor "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-rxjava2:$roomRxJavaVersion"
api "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
// Test
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$andxTestRunnerVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$andxTestEspressoVersion"
androidTestImplementation "androidx.test:core:$andxTestCoreVersion"
androidTestImplementation "androidx.test.ext:junit:$andxTestJunitVersion"
}

View file

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "343b0cb91d977d378c17c577a4dc231d",
"identityHash": "aa41a1d6727ebded374388bdd6cf8456",
"entities": [
{
"tableName": "accounts",
@ -121,7 +121,7 @@
},
{
"tableName": "contacts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk_contact_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fk_account_id` INTEGER NOT NULL, `fk_entity_id` INTEGER NOT NULL, `rostername` TEXT, `nickname` TEXT, `direction` TEXT, `sub_pending` INTEGER NOT NULL, `approved` INTEGER NOT NULL, FOREIGN KEY(`fk_account_id`) REFERENCES `accounts`(`pk_account_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`fk_entity_id`) REFERENCES `entities`(`pk_entity_id`) ON UPDATE NO ACTION ON DELETE RESTRICT )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk_contact_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fk_entity_id` INTEGER NOT NULL, `rostername` TEXT, `nickname` TEXT, `direction` TEXT, `sub_pending` INTEGER NOT NULL, `approved` INTEGER NOT NULL, FOREIGN KEY(`fk_entity_id`) REFERENCES `entities`(`pk_entity_id`) ON UPDATE NO ACTION ON DELETE RESTRICT )",
"fields": [
{
"fieldPath": "id",
@ -129,12 +129,6 @@
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "fk_account_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "entityId",
"columnName": "fk_entity_id",
@ -187,14 +181,6 @@
],
"createSql": "CREATE INDEX `index_contacts_pk_contact_id` ON `${TABLE_NAME}` (`pk_contact_id`)"
},
{
"name": "index_contacts_fk_account_id",
"unique": false,
"columnNames": [
"fk_account_id"
],
"createSql": "CREATE INDEX `index_contacts_fk_account_id` ON `${TABLE_NAME}` (`fk_account_id`)"
},
{
"name": "index_contacts_fk_entity_id",
"unique": true,
@ -214,17 +200,6 @@
}
],
"foreignKeys": [
{
"table": "accounts",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"fk_account_id"
],
"referencedColumns": [
"pk_account_id"
]
},
{
"table": "entities",
"onDelete": "RESTRICT",
@ -275,7 +250,7 @@
},
{
"tableName": "chats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk_chat_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fk_entity_id` INTEGER NOT NULL, `active` INTEGER NOT NULL, FOREIGN KEY(`fk_entity_id`) REFERENCES `entities`(`pk_entity_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk_chat_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fk_entity_id` INTEGER NOT NULL, `active` INTEGER NOT NULL, `last_read_message` INTEGER NOT NULL, `most_recent_mam_msg` TEXT, `earliest_mam_msg` TEXT, FOREIGN KEY(`fk_entity_id`) REFERENCES `entities`(`pk_entity_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
@ -294,6 +269,24 @@
"columnName": "active",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastReadMessageId",
"columnName": "last_read_message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mostRecentMamMessage",
"columnName": "most_recent_mam_msg",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "earliestMamMsg",
"columnName": "earliest_mam_msg",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
@ -525,7 +518,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '343b0cb91d977d378c17c577a4dc231d')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aa41a1d6727ebded374388bdd6cf8456')"
]
}
}

View file

@ -0,0 +1,51 @@
package org.mercury_im.messenger.persistence.room;
import android.content.Context;
import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.Before;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.persistence.room.repository.IAccountRepository;
import org.mercury_im.messenger.persistence.room.repository.IAvatarRepository;
import org.mercury_im.messenger.persistence.room.repository.IChatRepository;
import org.mercury_im.messenger.persistence.room.repository.IEntityCapsRepository;
import org.mercury_im.messenger.persistence.room.repository.IMessageRepository;
import org.mercury_im.messenger.persistence.room.repository.IRosterRepository;
public abstract class AbstractDatabaseTest {
protected AppDatabase db;
protected IAccountRepository accountRepository;
protected IRosterRepository rosterRepository;
protected IMessageRepository messageRepository;
protected IChatRepository chatRepository;
protected IEntityCapsRepository capsRepository;
protected IAvatarRepository avatarRepository;
protected final EntityBareJid TEST_JID_JULIET = JidCreate.entityBareFromOrThrowUnchecked("juliet@capulet.lit");
protected final EntityBareJid TEST_JID_ROMEO = JidCreate.entityBareFromOrThrowUnchecked("romeo@montague.lit");
protected final EntityBareJid TEST_JID_MERCUTIO = JidCreate.entityBareFromOrThrowUnchecked("mercutio@montague.lit");
@Before
public void createDb() {
Context context = ApplicationProvider.getApplicationContext();
db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
accountRepository = new IAccountRepository(db.accountDao());
rosterRepository = new IRosterRepository(
db.entityDao(), db.contactDao(), db.rosterInformationDao());
messageRepository = new IMessageRepository(db.messageDao());
chatRepository = new IChatRepository(db.chatDao(), rosterRepository);
capsRepository = new IEntityCapsRepository(db.entityCapsDao());
avatarRepository = new IAvatarRepository(db.avatarDao());
}
@After
public void closeDb() {
db.close();
}
}

View file

@ -1,24 +1,18 @@
package org.mercury_im.messenger.persistence.room;
import android.content.Context;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.jxmpp.jid.impl.JidCreate;
import org.mercury_im.messenger.persistence.room.model.RoomAccountModel;
import org.mercury_im.messenger.persistence.room.repository.IAccountRepository;
import org.mercury_im.messenger.persistence.room.repository.IRosterRepository;
import org.mercury_im.messenger.persistence.room.repository.IMessageRepository;
import java.util.List;
import org.mercury_im.messenger.persistence.room.model.RoomChatModel;
import org.mercury_im.messenger.persistence.room.model.RoomContactModel;
import org.mercury_im.messenger.persistence.room.model.RoomEntityModel;
import io.reactivex.Observable;
import io.reactivex.functions.Predicate;
import static org.junit.Assert.assertEquals;
import io.reactivex.disposables.Disposable;
/**
* Instrumented test, which will execute on an Android device.
@ -26,33 +20,67 @@ import static org.junit.Assert.assertEquals;
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
public class ExampleInstrumentedTest extends AbstractDatabaseTest {
@Test
public void useAppContext() {
// Context of the app under test
public void testUpsertContact() {
RoomAccountModel accountModel = new RoomAccountModel();
accountModel.setJid(TEST_JID_JULIET);
accountRepository.insertAccount(accountModel).test().assertValue(1L).dispose();
Context context = ApplicationProvider.getApplicationContext();
RoomContactModel contactModel = new RoomContactModel();
RoomEntityModel entityModel = new RoomEntityModel();
entityModel.setAccountId(1);
entityModel.setJid(TEST_JID_ROMEO);
contactModel.setEntity(entityModel);
AppDatabase appDatabase = AppDatabase.getDatabase(context);
IAccountRepository accountRepository = new IAccountRepository(appDatabase.accountDao());
IRosterRepository rosterRepository = new IRosterRepository(appDatabase.rosterEntryDao());
IMessageRepository messageRepository = new IMessageRepository(appDatabase.messageDao());
rosterRepository.upsertContact(contactModel).test().assertValue(1L).dispose();
rosterRepository.getContact(1L).subscribe(contact -> {
System.out.println(contact.getEntity().getAccountId() + " " + contact.getEntity().getJid().toString());
}).dispose();
Observable<List<RoomAccountModel>> accounts = accountRepository.getAllAccounts();
rosterRepository.upsertContact(contactModel).test().assertValue(1L).dispose();
rosterRepository.getContact(1L).subscribe(contact -> {
System.out.println(contact.getEntity().getAccountId() + " " + contact.getEntity().getJid().toString());
}).dispose();
}
final RoomAccountModel a1 = new RoomAccountModel();
a1.setJid(JidCreate.entityBareFromOrThrowUnchecked("alice@wonderland.lit"));
a1.setPassword("5w0rdf1sh");
a1.setEnabled(false);
accountRepository.insertAccount(a1);
@Test
public void testGetOrCreateEntity() {
RoomAccountModel accountModel = new RoomAccountModel();
accountModel.setJid(TEST_JID_JULIET);
accountRepository.insertAccount(accountModel).test().assertValue(1L).dispose();
accounts.test()
.assertValue(new Predicate<List<RoomAccountModel>>() {
@Override
public boolean test(List<RoomAccountModel> roomAccountModels) throws Exception {
return roomAccountModels.size() == 1;
}
});
RoomEntityModel romeo = rosterRepository.getOrCreateEntityForAccountAndJid(accountModel, TEST_JID_ROMEO).blockingGet();
RoomEntityModel mercu = rosterRepository.getOrCreateEntityForAccountAndJid(accountModel, TEST_JID_MERCUTIO).blockingGet();
}
@Test
public void testObservabilityOfChat() throws InterruptedException {
RoomAccountModel accountModel = new RoomAccountModel();
accountModel.setJid(TEST_JID_JULIET);
accountRepository.insertAccount(accountModel).test().assertValue(1L).dispose();
Observable<RoomChatModel> chat = chatRepository.getOrCreateChatWith(accountModel.getId(), TEST_JID_ROMEO);
Disposable disposable = chat.subscribe(chatModel -> Log.d(AppDatabase.TAG, "onNext: " + chatModel));
RoomChatModel chatModel = chatRepository.newChatModel();
chatModel.setId(1L);
chatModel.setPeerEntityId(1L);
chatModel.setActive(true);
chatRepository.updateChat(chatModel).blockingAwait();
chatModel.setActive(false);
chatRepository.updateChat(chatModel).blockingAwait();
chatModel.setActive(true);
chatRepository.updateChat(chatModel).blockingAwait();
Thread.sleep(100);
disposable.dispose();
Thread.sleep(100);
}
}

View file

@ -1,7 +1,6 @@
package org.mercury_im.messenger.persistence.room;
import android.content.Context;
import android.util.Log;
import androidx.room.Database;
import androidx.room.Room;
@ -43,6 +42,8 @@ public abstract class AppDatabase extends RoomDatabase {
private static final String DB_NAME = "mercury_db";
private static AppDatabase INSTANCE;
public static final String TAG = "PERSISTENCE_ROOM";
public static AppDatabase getDatabase(final Context context) {
if (INSTANCE == null) {
Logger.getLogger("DATABASE").log(Level.INFO, context.getApplicationContext().getDatabasePath(DB_NAME).getAbsolutePath());

View file

@ -3,9 +3,9 @@ package org.mercury_im.messenger.persistence.room;
import org.mercury_im.messenger.persistence.repository.AccountRepository;
import org.mercury_im.messenger.persistence.repository.AvatarRepository;
import org.mercury_im.messenger.persistence.repository.ChatRepository;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
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.persistence.room.dao.AccountDao;
import org.mercury_im.messenger.persistence.room.dao.AvatarDao;
import org.mercury_im.messenger.persistence.room.dao.ChatDao;
@ -17,9 +17,9 @@ import org.mercury_im.messenger.persistence.room.dao.RosterInformationDao;
import org.mercury_im.messenger.persistence.room.repository.IAccountRepository;
import org.mercury_im.messenger.persistence.room.repository.IAvatarRepository;
import org.mercury_im.messenger.persistence.room.repository.IChatRepository;
import org.mercury_im.messenger.persistence.room.repository.IRosterRepository;
import org.mercury_im.messenger.persistence.room.repository.IEntityCapsRepository;
import org.mercury_im.messenger.persistence.room.repository.IMessageRepository;
import org.mercury_im.messenger.persistence.room.repository.IRosterRepository;
import javax.inject.Singleton;
@ -37,8 +37,8 @@ public class RoomRepositoryModule {
@Singleton
@Provides
ChatRepository provideChatRepository(ChatDao chatDao, EntityDao entityDao) {
return new IChatRepository(chatDao, entityDao);
ChatRepository provideChatRepository(ChatDao chatDao, RosterRepository rosterRepository) {
return new IChatRepository(chatDao, rosterRepository);
}
@Singleton

View file

@ -36,7 +36,10 @@ public interface AccountDao extends BaseDao<RoomAccountModel> {
* @return account or null
*/
@Query("select * from accounts where pk_account_id = :id")
Maybe<RoomAccountModel> getAccountById(long id);
Maybe<RoomAccountModel> maybeGetAccountById(long id);
@Query("select * from accounts where pk_account_id = :id")
Observable<RoomAccountModel> getAccountById(long id);
@Query("select * from accounts where jid = :jid")
Maybe<RoomAccountModel> getAccountByJid(EntityBareJid jid);

View file

@ -2,16 +2,27 @@ package org.mercury_im.messenger.persistence.room.dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Transaction;
import androidx.room.Update;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
public interface BaseDao<T> {
@Insert
@Insert(onConflict = OnConflictStrategy.IGNORE)
Single<Long> insert(T entity);
@Insert(onConflict = OnConflictStrategy.IGNORE)
Single<Long[]> insert(T[] entities);
@Insert(onConflict = OnConflictStrategy.IGNORE)
Single<List<Long>> insert(List<T> entities);
@Update
Completable update(T entity);
@ -23,4 +34,7 @@ public interface BaseDao<T> {
@Delete
Completable delete(T[] entities);
@Delete
Completable delete(List<T> entities);
}

View file

@ -13,6 +13,7 @@ import java.util.List;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Single;
@Dao
@TypeConverters(EntityBareJidConverter.class)
@ -22,26 +23,23 @@ public interface ChatDao extends BaseDao<RoomChatModel> {
Observable<List<RoomChatModel>> getAllChats();
@Query("SELECT chats.* FROM chats JOIN entities WHERE fk_account_id = :accountId")
Observable<List<RoomChatModel>> getAllChatsOf(long accountId);
Observable<List<RoomChatModel>> getAllChatsOfAccount(long accountId);
@Query("SELECT * FROM chats WHERE fk_entity_id = :entityId")
Maybe<RoomChatModel> getChatWithIdentity(long entityId);
Maybe<RoomChatModel> maybeGetChatWithEntity(long entityId);
@Query("SELECT * FROM chats WHERE fk_entity_id = :entityId")
Observable<RoomChatModel> getChatWithEntity(long entityId);
@Query("SELECT chats.* FROM chats JOIN entities WHERE fk_account_id = :accountId AND jid = :jid")
Maybe<RoomChatModel> getChatWithJid(long accountId, EntityBareJid jid);
Maybe<RoomChatModel> maybeGetChatWithJid(long accountId, EntityBareJid jid);
@Query("SELECT chats.* FROM chats JOIN entities WHERE fk_account_id = :accountId AND jid = :jid")
Observable<RoomChatModel> getChatWithJid(long accountId, EntityBareJid jid);
@Query("SELECT chats.* FROM chats JOIN contacts WHERE contacts.pk_contact_id = :contactId")
Maybe<RoomChatModel> getChatWithContact(long contactId);
@Query("SELECT chats.* FROM chats JOIN entities WHERE fk_account_id = :accountId AND jid = :jid")
Observable<RoomChatModel> getObservableChatWithJid(long accountId, EntityBareJid jid);
@Query("SELECT " +
"pk_chat_id as chatId, " +
"fk_entity_id as entityId, " +
"active, " +
"jid " +
"FROM chats INNER JOIN entities")
Observable<Chat> getChat();
Maybe<RoomChatModel> maybeGetChatWithContact(long contactId);
@Query("SELECT chats.* FROM chats JOIN contacts WHERE contacts.pk_contact_id = :contactId")
Observable<RoomChatModel> getChatWithContact(long contactId);
}

View file

@ -37,10 +37,11 @@ public interface ContactDao extends BaseDao<RoomContactModel> {
Observable<List<RoomContactModel>> getAllContacts();
@Query("SELECT contacts.* FROM contacts JOIN entities " +
"WHERE contacts.fk_account_id = :accountId AND jid = :jid")
"WHERE entities.fk_account_id = :accountId AND jid = :jid")
Maybe<RoomContactModel> getContactByJid(long accountId, EntityBareJid jid);
@Query("SELECT * FROM contacts WHERE fk_account_id = :accountId")
@Query("SELECT contacts.* FROM contacts JOIN entities " +
"WHERE entities.fk_account_id = :accountId")
Observable<List<RoomContactModel>> getContactsForAccount(long accountId);
@Query("DELETE FROM contacts WHERE pk_contact_id = :id")
@ -52,12 +53,15 @@ public interface ContactDao extends BaseDao<RoomContactModel> {
@Query("DELETE FROM contacts")
Completable deleteAll();
@Query("DELETE FROM contacts WHERE fk_account_id = :accountId")
@Query("DELETE FROM contacts WHERE fk_entity_id IN " +
"(SELECT pk_entity_id FROM entities " +
"WHERE entities.fk_account_id = :accountId)")
Completable deleteAllForAccount(long accountId);
@Query("DELETE FROM contacts WHERE pk_contact_id IN(:ids)")
Completable deleteContacts(long[] ids);
@Query("SELECT entities.* FROM contacts INNER JOIN entities ON contacts.fk_entity_id = entities.pk_entity_id WHERE contacts.pk_contact_id = :contactId")
@Query("SELECT entities.* FROM contacts INNER JOIN entities ON contacts.fk_entity_id = entities.pk_entity_id " +
"WHERE contacts.pk_contact_id = :contactId")
Single<RoomEntityModel> getEntityForContactId(long contactId);
}

View file

@ -2,8 +2,6 @@ package org.mercury_im.messenger.persistence.room.dao;
import androidx.annotation.WorkerThread;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import org.mercury_im.messenger.persistence.room.model.RoomEntityCapsModel;
@ -11,31 +9,18 @@ import org.mercury_im.messenger.persistence.room.model.RoomEntityCapsModel;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
import static androidx.room.OnConflictStrategy.REPLACE;
@Dao
@WorkerThread
public interface EntityCapsDao {
public interface EntityCapsDao extends BaseDao<RoomEntityCapsModel> {
@Query("SELECT * FROM entity_caps WHERE pk_node_ver = :nodeVer")
Single<RoomEntityCapsModel> getEntityCapsForNodeVer(String nodeVer);
@Query("SELECT * FROM entity_caps")
Single<List<RoomEntityCapsModel>> getAllEntityCaps();
@Insert(onConflict = REPLACE)
Completable insert(List<RoomEntityCapsModel> models);
@Insert(onConflict = REPLACE)
Completable insert(RoomEntityCapsModel model);
@Delete
Completable delete(List<RoomEntityCapsModel> models);
@Delete
Completable delete(RoomEntityCapsModel model);
Observable<List<RoomEntityCapsModel>> getAllEntityCaps();
@Query("DELETE FROM entity_caps")
Completable deleteAllEntityCaps();

View file

@ -18,10 +18,6 @@ import static androidx.room.OnConflictStrategy.REPLACE;
@TypeConverters(EntityBareJidConverter.class)
public interface EntityDao extends BaseDao<RoomEntityModel> {
@Override
@Insert(onConflict = REPLACE)
Single<Long> insert(RoomEntityModel entity);
@Query("SELECT * FROM entities WHERE pk_entity_id = :id")
Maybe<RoomEntityModel> getEntity(long id);

View file

@ -1,6 +1,7 @@
package org.mercury_im.messenger.persistence.room.model;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Index;
@ -48,8 +49,9 @@ public class RoomAccountModel extends AbstractAccountModel {
}
@Override
public void setId(long id) {
public RoomAccountModel setId(long id) {
this.id = id;
return this;
}
@Override
@ -91,4 +93,16 @@ public class RoomAccountModel extends AbstractAccountModel {
public void setState(String state) {
this.state = state;
}
@Override
@NonNull
public String toString() {
return "AccountModel[" +
KEY_ID + ": " + getId() + ", " +
KEY_JID + ": " + (getJid() != null ? getJid().toString() : "null") + ", " +
KEY_PASSWORD + ": " + getPassword() + ", " +
KEY_ENABLED + ": " + getEnabled() + ", " +
KEY_STATE + ": " +getState() +
"]";
}
}

View file

@ -1,5 +1,6 @@
package org.mercury_im.messenger.persistence.room.model;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
@ -30,6 +31,9 @@ public class RoomChatModel implements ChatModel {
public static final String KEY_ENTITY = "fk_entity_id";
public static final String KEY_ACTIVE = "active";
public static final String KEY_LAST_READ_MSG = "last_read_message";
public static final String KEY_MOST_RECENT_MAM_MSG = "most_recent_mam_msg";
public static final String KEY_EARLIEST_MAM_MSG = "earliest_mam_msg";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = KEY_ID)
@ -41,6 +45,15 @@ public class RoomChatModel implements ChatModel {
@ColumnInfo(name = KEY_ACTIVE)
private boolean isActive;
@ColumnInfo(name = KEY_LAST_READ_MSG)
private long lastReadMessageId;
@ColumnInfo(name = KEY_MOST_RECENT_MAM_MSG)
private String mostRecentMamMessageId;
@ColumnInfo(name = KEY_EARLIEST_MAM_MSG)
private String earliestMamMessageId;
@Override
public long getId() {
return id;
@ -70,4 +83,44 @@ public class RoomChatModel implements ChatModel {
public void setActive(boolean active) {
this.isActive = active;
}
@Override
public long getLastReadMessageId() {
return lastReadMessageId;
}
@Override
public void setLastReadMessageId(long messageId) {
this.lastReadMessageId = messageId;
}
@Override
public String getMostRecentMamMessageId() {
return mostRecentMamMessageId;
}
@Override
public void setMostRecentMamMessageId(String uid) {
this.mostRecentMamMessageId = uid;
}
@Override
public String getEarliestMamMessageId() {
return earliestMamMessageId;
}
@Override
public void setEarliestMamMessageId(String uid) {
this.earliestMamMessageId = uid;
}
@Override
@NonNull
public String toString() {
return "ChatModel[" +
KEY_ID + ": " + getId() + ", " +
KEY_ENTITY + ": " + getPeerEntityId() + ", " +
KEY_ACTIVE + ": " + isActive() +
"]";
}
}

View file

@ -1,6 +1,7 @@
package org.mercury_im.messenger.persistence.room.model;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
@ -13,9 +14,7 @@ import org.mercury_im.messenger.persistence.model.ContactModel;
import org.mercury_im.messenger.persistence.model.EntityModel;
import org.mercury_im.messenger.persistence.room.type_converter.DirectionConverter;
import static androidx.room.ForeignKey.CASCADE;
import static androidx.room.ForeignKey.RESTRICT;
import static org.mercury_im.messenger.persistence.room.model.RoomContactModel.KEY_ACCOUNT_ID;
import static org.mercury_im.messenger.persistence.room.model.RoomContactModel.KEY_ID;
import static org.mercury_im.messenger.persistence.room.model.RoomContactModel.KEY_ENTITY_ID;
import static org.mercury_im.messenger.persistence.room.model.RoomContactModel.TABLE;
@ -23,15 +22,10 @@ import static org.mercury_im.messenger.persistence.room.model.RoomContactModel.T
@Entity(tableName = TABLE,
indices = {
@Index(value = KEY_ID),
@Index(value = KEY_ACCOUNT_ID),
@Index(value = KEY_ENTITY_ID, unique = true),
@Index(value = {KEY_ID, KEY_ENTITY_ID}, unique = true)
},
foreignKeys = {
@ForeignKey(entity = RoomAccountModel.class,
parentColumns = RoomAccountModel.KEY_ID,
childColumns = KEY_ACCOUNT_ID,
onDelete = CASCADE),
@ForeignKey(entity = RoomEntityModel.class,
parentColumns = RoomEntityModel.KEY_ID,
childColumns = KEY_ENTITY_ID,
@ -40,21 +34,17 @@ public class RoomContactModel implements ContactModel {
public static final String TABLE = "contacts";
public static final String KEY_ID = "pk_contact_id";
public static final String KEY_ACCOUNT_ID = "fk_account_id";
public static final String KEY_ENTITY_ID = "fk_entity_id";
public static final String KEY_ROSTER_NAME = "rostername";
public static final String KEY_NICKNAME = "nickname";
public static final String KEY_DIRECTION = "direction";
public static final String KEY_SUB_DIRECTION = "direction";
public static final String KEY_SUB_PENDING = "sub_pending";
public static final String KEY_APPROVED = "approved";
public static final String KEY_SUB_APPROVED = "approved";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = KEY_ID)
private long id;
@ColumnInfo(name = KEY_ACCOUNT_ID)
private long accountId;
@ColumnInfo(name = KEY_ENTITY_ID)
private long entityId;
@ -64,14 +54,14 @@ public class RoomContactModel implements ContactModel {
@ColumnInfo(name = KEY_NICKNAME)
private String nickname;
@ColumnInfo(name = KEY_DIRECTION)
@ColumnInfo(name = KEY_SUB_DIRECTION)
@TypeConverters(DirectionConverter.class)
private DIRECTION direction;
@ColumnInfo(name = KEY_SUB_PENDING)
private boolean subscriptionPending;
@ColumnInfo(name = KEY_APPROVED)
@ColumnInfo(name = KEY_SUB_APPROVED)
private boolean approved;
/**
@ -91,16 +81,6 @@ public class RoomContactModel implements ContactModel {
this.id = id;
}
@Override
public long getAccountId() {
return accountId;
}
@Override
public void setAccountId(long accountId) {
this.accountId = accountId;
}
@Override
public long getEntityId() {
return entityId;
@ -169,5 +149,20 @@ public class RoomContactModel implements ContactModel {
@Override
public void setEntity(EntityModel entity) {
this.entityModel = entity;
this.entityId = entity.getId();
}
@Override
@NonNull
public String toString() {
return "ContactModel[" +
KEY_ID + ": " + getId() + ", " +
KEY_ENTITY_ID + ": " + getEntityId() + ", " +
KEY_ROSTER_NAME + ": " + getRosterName() + ", " +
KEY_NICKNAME + ": " + getNickname() + ", " +
KEY_SUB_DIRECTION + ": " + getDirection() + ", " +
KEY_SUB_PENDING + ": " + isSubscriptionPending() + ", " +
KEY_SUB_APPROVED + ": " + isApproved() +
"]";
}
}

View file

@ -38,7 +38,6 @@ public class RoomEntityModel implements EntityModel {
public static final String KEY_ID = "pk_entity_id";
public static final String KEY_ACCOUNT_ID = "fk_account_id";
public static final String KEY_JID = "jid";
public static final String KEY_AVATAR = "avatar";
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = KEY_ID)
@ -82,4 +81,14 @@ public class RoomEntityModel implements EntityModel {
public void setAccountId(long accountId) {
this.accountId = accountId;
}
@Override
@NonNull
public String toString() {
return "EntityModel[" +
KEY_ID + ": " + getId() + ", " +
KEY_ACCOUNT_ID + ": " + getAccountId() + ", " +
KEY_JID + ": " + getJid().toString() +
"]";
}
}

View file

@ -1,5 +1,6 @@
package org.mercury_im.messenger.persistence.room.model;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Index;
@ -45,4 +46,13 @@ public class RoomRosterInformationModel implements RosterInformationModel {
public void setRosterVersion(String rosterVersion) {
this.rosterVersion = rosterVersion;
}
@Override
@NonNull
public String toString() {
return "RosterInformationModel[" +
KEY_ID + ": " + getAccountId() + ", " +
KEY_ROSTER_VERSION + ": " + getRosterVersion() +
"]";
}
}

View file

@ -6,5 +6,5 @@ public class Chat {
public long chatId;
public long entityId;
public EntityBareJid jid;
public boolean active;
}

View file

@ -1,5 +1,9 @@
package org.mercury_im.messenger.persistence.room.repository;
import android.util.Log;
import androidx.annotation.NonNull;
import org.mercury_im.messenger.persistence.repository.AccountRepository;
import org.mercury_im.messenger.persistence.room.dao.AccountDao;
import org.mercury_im.messenger.persistence.room.model.RoomAccountModel;
@ -13,6 +17,8 @@ import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Single;
import static org.mercury_im.messenger.persistence.room.AppDatabase.TAG;
public class IAccountRepository implements AccountRepository<RoomAccountModel> {
private final AccountDao accountDao;
@ -28,10 +34,15 @@ public class IAccountRepository implements AccountRepository<RoomAccountModel> {
}
@Override
public Maybe<RoomAccountModel> getAccount(long accountId) {
public Observable<RoomAccountModel> getAccount(long accountId) {
return accountDao.getAccountById(accountId);
}
@Override
public Maybe<RoomAccountModel> maybeGetAccount(long accountId) {
return accountDao.maybeGetAccountById(accountId);
}
@Override
public Observable<List<RoomAccountModel>> getAllAccounts() {
return accountDao.getAllAccounts();
@ -39,13 +50,16 @@ public class IAccountRepository implements AccountRepository<RoomAccountModel> {
@Override
public Single<Long> insertAccount(RoomAccountModel accountModel) {
return accountDao.insert(accountModel);
public Single<Long> insertAccount(@NonNull RoomAccountModel accountModel) {
return accountDao.insert(accountModel)
.map(accountId -> accountModel.setId(accountId).getId())
.doOnSubscribe(ignore -> Log.v(TAG, "Insert " + accountModel))
.doAfterSuccess(accountId -> Log.v(TAG, "AccountModel has new ID " + accountId));
}
@Override
public Completable updateState(long accountId, String state) {
return accountDao.updateConnectionState(accountId, state);
public Completable updateAccount(RoomAccountModel accountModel) {
return accountDao.update(accountModel);
}
@Override

View file

@ -1,15 +1,13 @@
package org.mercury_im.messenger.persistence.room.repository;
import android.util.Log;
import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.persistence.model.AccountModel;
import org.mercury_im.messenger.persistence.model.ChatModel;
import org.mercury_im.messenger.persistence.model.ContactModel;
import org.mercury_im.messenger.persistence.model.EntityModel;
import org.mercury_im.messenger.persistence.repository.ChatRepository;
import org.mercury_im.messenger.persistence.repository.RosterRepository;
import org.mercury_im.messenger.persistence.room.dao.ChatDao;
import org.mercury_im.messenger.persistence.room.dao.EntityDao;
import org.mercury_im.messenger.persistence.room.model.RoomChatModel;
import org.mercury_im.messenger.persistence.room.model.RoomEntityModel;
import java.util.List;
@ -18,21 +16,19 @@ import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.SingleTransformer;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.Single;
import static org.mercury_im.messenger.persistence.room.AppDatabase.TAG;
public class IChatRepository implements ChatRepository<RoomChatModel> {
private final ChatDao chatDao;
private final EntityDao entityDao;
private final RosterRepository rosterRepository;
@Inject
public IChatRepository(ChatDao chatDao, EntityDao entityDao) {
public IChatRepository(ChatDao chatDao, RosterRepository rosterRepository) {
this.chatDao = chatDao;
this.entityDao = entityDao;
this.rosterRepository = rosterRepository;
}
@Override
@ -42,7 +38,36 @@ public class IChatRepository implements ChatRepository<RoomChatModel> {
@Override
public Observable<RoomChatModel> getOrCreateChatWith(long accountId, EntityBareJid jid) {
return Observable.fromCallable(() -> {
EntityModel entity = (EntityModel) rosterRepository.getOrCreateEntityForAccountAndJid(accountId, jid).blockingGet();
RoomChatModel chat = maybeGetChatWithEntity(entity).blockingGet();
if (chat == null) {
chat = newChatModel();
chat.setPeerEntityId(entity.getId());
chat.setId(insertChat(chat).blockingGet());
}
return chat;
}).concatWith(getChatWith(accountId, jid).skip(1));
}
@Override
public Observable<RoomChatModel> getChatWith(long accountId, EntityBareJid jid) {
return chatDao.getChatWithJid(accountId, jid);
}
@Override
public Observable<RoomChatModel> getChatWithEntity(long entityId) {
return chatDao.getChatWithEntity(entityId);
}
@Override
public Completable updateChat(RoomChatModel chat) {
return chatDao.update(chat);
}
@Override
public Observable<RoomChatModel> getChatWithContact(long contactId) {
return chatDao.getChatWithContact(contactId);
}
@Override
@ -51,27 +76,39 @@ public class IChatRepository implements ChatRepository<RoomChatModel> {
}
@Override
public Observable<List<RoomChatModel>> getAllChatsOf(AccountModel accountModel) {
return chatDao.getAllChatsOf(accountModel.getId());
public Observable<List<RoomChatModel>> getAllChatsOfAccount(long accountId) {
return chatDao.getAllChatsOfAccount(accountId);
}
@Override
public Maybe<RoomChatModel> getChatWith(EntityModel identity) {
return chatDao.getChatWithIdentity(identity.getId());
public Maybe<RoomChatModel> maybeGetChatWith(long accountId, EntityBareJid jid) {
return chatDao.maybeGetChatWithJid(accountId, jid);
}
@Override
public Maybe<RoomChatModel> getChatWith(AccountModel account, EntityBareJid jid) {
return chatDao.getChatWithJid(account.getId(), jid);
public Maybe<RoomChatModel> maybeGetChatWithEntity(long entityId) {
return chatDao.maybeGetChatWithEntity(entityId);
}
@Override
public Maybe<RoomChatModel> getChatWith(ContactModel contact) {
return chatDao.getChatWithContact(contact.getId());
public Maybe<RoomChatModel> maybeGetChatWithContact(long contactId) {
return chatDao.maybeGetChatWithContact(contactId);
}
@Override
public Single<Long> insertChat(RoomChatModel chat) {
return chatDao.insert(chat)
.map(chatId -> {
chat.setId(chatId);
return chatId;
})
.doOnSubscribe(ignore -> Log.v(TAG, "Insert " + chat))
.doAfterSuccess(chatId -> Log.v(TAG, "Assign ID " + chatId + " to " + chat));
}
@Override
public Completable closeChat(RoomChatModel chat) {
return chatDao.delete(chat);
}
}

View file

@ -11,6 +11,7 @@ import java.util.List;
import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -29,7 +30,7 @@ public class IEntityCapsRepository implements EntityCapsRepository<RoomEntityCap
}
@Override
public Single<List<RoomEntityCapsModel>> getAllEntityCaps() {
public Observable<List<RoomEntityCapsModel>> getAllEntityCaps() {
return dao.getAllEntityCaps();
}
@ -39,12 +40,12 @@ public class IEntityCapsRepository implements EntityCapsRepository<RoomEntityCap
}
@Override
public Completable insertOrReplaceEntityCaps(List<RoomEntityCapsModel> entityCaps) {
public Single<List<Long>> insertOrReplaceEntityCaps(List<RoomEntityCapsModel> entityCaps) {
return dao.insert(entityCaps);
}
@Override
public Completable insertOrReplaceEntityCaps(RoomEntityCapsModel entityCaps) {
public Single<Long> insertOrReplaceEntityCaps(RoomEntityCapsModel entityCaps) {
return dao.insert(entityCaps);
}

View file

@ -37,6 +37,11 @@ public class IMessageRepository implements MessageRepository<RoomMessageModel> {
return messageDao.insert(message);
}
@Override
public Single<List<Long>> insertMessages(List<RoomMessageModel> messages) {
return messageDao.insert(messages);
}
@Override
public Observable<RoomMessageModel> getLastMessageFrom(long accountId, EntityBareJid peer) {
return messageDao.getLastMessageFrom(accountId, peer);

View file

@ -12,18 +12,16 @@ import org.mercury_im.messenger.persistence.room.model.RoomEntityModel;
import org.mercury_im.messenger.persistence.room.model.RoomRosterInformationModel;
import java.util.List;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.CompletableSource;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.SingleSource;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import static org.mercury_im.messenger.persistence.room.AppDatabase.TAG;
public class IRosterRepository extends RosterRepository<RoomEntityModel, RoomContactModel, RoomRosterInformationModel> {
@ -72,21 +70,43 @@ public class IRosterRepository extends RosterRepository<RoomEntityModel, RoomCon
}
@Override
public Single<Long> updateOrInsertContact(RoomContactModel contact) {
//noinspection unchecked
return entityDao.getEntityFor(contact.getAccountId(), contact.getEntity().getJid())
.switchIfEmpty(insertOrReplaceEntity((RoomEntityModel) contact.getEntity())
.flatMap(entityId -> {
Log.d("Mercury", "Insert entity since maybe did not emit: " + entityId);
contact.getEntity().setId(entityId);
return (SingleSource<RoomEntityModel>) observer ->
observer.onSuccess((RoomEntityModel) contact.getEntity());
}))
.flatMap(entityModel -> {
contact.setEntityId(entityModel.getId());
contact.setEntity(entityModel);
return contactDao.insert(contact);
});
public Single<Long> upsertContact(RoomContactModel contact) {
return Single.fromCallable(() -> {
RoomEntityModel existingEntityModel = entityDao
.getEntityFor(contact.getEntity().getAccountId(), contact.getEntity().getJid())
.blockingGet();
if (existingEntityModel == null) {
// Insert missing entity
existingEntityModel = (RoomEntityModel) contact.getEntity();
long entityId = insertEntity(existingEntityModel).blockingGet();
existingEntityModel.setId(entityId);
contact.setEntity(existingEntityModel);
}
RoomContactModel existingContactModel = contactDao.getContactForEntityId(existingEntityModel.getId()).blockingGet();
if (existingContactModel == null) {
// Insert missing contact
existingContactModel = contact;
return insertContact(existingContactModel).blockingGet();
} else {
contactDao.update(contact);
}
return existingContactModel.getId();
});
}
@Override
public Single<Long> insertContact(RoomContactModel contact) {
return contactDao.insert(contact)
.map(contactId -> {
contact.setId(contactId);
return contactId;
})
.doOnSubscribe(ignore -> Log.v(TAG, "Insert " + contact))
.doAfterSuccess(cid -> Log.v(TAG, "Assigned ID " + cid + " to " + contact));
}
@Override
@ -107,6 +127,20 @@ public class IRosterRepository extends RosterRepository<RoomEntityModel, RoomCon
});
}
@Override
public Single<RoomEntityModel> getOrCreateEntityForAccountAndJid(long accountId, EntityBareJid jid) {
return Single.fromCallable(() -> {
RoomEntityModel existing = getEntityForAccountAndJid(accountId, jid).blockingGet();
if (existing == null) {
existing = newEntityModel();
existing.setAccountId(accountId);
existing.setJid(jid);
existing.setId(insertEntity(existing).blockingGet());
}
return existing;
});
}
@Override
public Maybe<RoomContactModel> getContactForEntity(long entityId) {
return contactDao.getContactForEntityId(entityId)
@ -133,13 +167,8 @@ public class IRosterRepository extends RosterRepository<RoomEntityModel, RoomCon
public Completable deleteContact(long accountId, EntityBareJid jid) {
// Since Room does not support "DELETE x FROM X x INNER JOIN Y...", we have to get the
// entity for the jid first and then delete by using its entityId
final Maybe<RoomEntityModel> entity = getEntity(accountId, jid.asEntityBareJidOrThrow());
return entity.flatMapCompletable(new Function<RoomEntityModel, CompletableSource>() {
@Override
public CompletableSource apply(RoomEntityModel entityModel) {
return contactDao.deleteContactForEntity(entityModel.getId());
}
});
final Maybe<RoomEntityModel> entity = getEntityForAccountAndJid(accountId, jid.asEntityBareJidOrThrow());
return entity.flatMapCompletable(entityModel -> contactDao.deleteContactForEntity(entityModel.getId()));
}
@Override
@ -173,7 +202,8 @@ public class IRosterRepository extends RosterRepository<RoomEntityModel, RoomCon
@Override
public Single<Long> updateRosterInformation(RoomRosterInformationModel rosterInformation) {
return rosterInformationDao.insertRosterInformation(rosterInformation);
return rosterInformationDao.insertRosterInformation(rosterInformation)
.doOnSubscribe(ignore -> Log.v(TAG, "Insert " + rosterInformation));
}
/*
@ -191,13 +221,18 @@ public class IRosterRepository extends RosterRepository<RoomEntityModel, RoomCon
}
@Override
public Maybe<RoomEntityModel> getEntity(long accountId, EntityBareJid jid) {
public Maybe<RoomEntityModel> getEntityForAccountAndJid(long accountId, EntityBareJid jid) {
return entityDao.getEntityFor(accountId, jid);
}
@Override
public Single<Long> insertOrReplaceEntity(RoomEntityModel entity) {
return entityDao.insert(entity);
public Single<Long> insertEntity(RoomEntityModel entityModel) {
return entityDao.insert(entityModel)
.map(entityId -> {
entityModel.setId(entityId);
return entityId;
})
.doOnSubscribe(ignore -> Log.v(TAG, "Insert " + entityModel))
.doAfterSuccess(entityId -> Log.v(TAG, "Assign ID " + entityId + " to " + entityModel));
}
}

View file

@ -19,7 +19,7 @@ public interface AccountModel {
*
* @param id account id
*/
void setId(long id);
AccountModel setId(long id);
/**
* Return the password of the XMPP account.

View file

@ -13,4 +13,16 @@ public interface ChatModel {
boolean isActive();
void setActive(boolean opened);
long getLastReadMessageId();
void setLastReadMessageId(long messageId);
String getMostRecentMamMessageId();
void setMostRecentMamMessageId(String uid);
String getEarliestMamMessageId();
void setEarliestMamMessageId(String uid);
}

View file

@ -6,10 +6,6 @@ public interface ContactModel<E extends EntityModel> {
void setId(long id);
long getAccountId();
void setAccountId(long id);
long getEntityId();
void setEntityId(long id);

View file

@ -13,13 +13,15 @@ public interface AccountRepository<E extends AccountModel> {
E newAccountModel();
Maybe<E> getAccount(long accountId);
Observable<E> getAccount(long accountId);
Maybe<E> maybeGetAccount(long accountId);
Observable<List<E>> getAllAccounts();
Single<Long> insertAccount(E accountModel);
Completable updateState(long accountId, String state);
Completable updateAccount(E accountModel);
Completable deleteAccount(E item);

View file

@ -11,6 +11,7 @@ import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Single;
public interface ChatRepository<E extends ChatModel> {
@ -18,15 +19,60 @@ public interface ChatRepository<E extends ChatModel> {
Observable<List<E>> getAllChats();
Observable<List<E>> getAllChatsOf(AccountModel account);
Observable<List<E>> getAllChatsOfAccount(long accountId);
default Observable<List<E>> getAllChatsOfAccount(AccountModel account) {
return getAllChatsOfAccount(account.getId());
}
Observable<E> getOrCreateChatWith(long accountId, EntityBareJid jid);
Maybe<E> getChatWith(AccountModel account, EntityBareJid jid);
Observable<E> getChatWith(long accountId, EntityBareJid jid);
Maybe<E> getChatWith(EntityModel identity);
default Observable<E> getChatWith(AccountModel accountModel, EntityBareJid jid) {
return getChatWith(accountModel.getId(), jid);
}
Maybe<E> getChatWith(ContactModel contact);
Observable<E> getChatWithEntity(long entityId);
default Observable<E> getChatWithEntity(EntityModel entity) {
return getChatWithEntity(entity.getId());
}
Observable<E> getChatWithContact(long contactId);
default Observable<E> getChatWithContact(ContactModel contact) {
return getChatWithContact(contact.getId());
}
Maybe<E> maybeGetChatWith(long accountId, EntityBareJid jid);
default Maybe<E> maybeGetChatWith(AccountModel account, EntityBareJid jid) {
return maybeGetChatWith(account.getId(), jid);
}
Maybe<E> maybeGetChatWithContact(long contactId);
default Maybe<E> maybeGetChatWithContact(ContactModel contact) {
return maybeGetChatWithContact(contact.getId());
}
Maybe<E> maybeGetChatWithEntity(long entityId);
default Maybe<E> maybeGetChatWithEntity(EntityModel entity) {
return maybeGetChatWithEntity(entity.getId());
}
Completable updateChat(E chat);
Single<Long> insertChat(E chat);
Completable closeChat(E chat);
}

View file

@ -5,19 +5,20 @@ import org.mercury_im.messenger.persistence.model.EntityCapsModel;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
public interface EntityCapsRepository<E extends EntityCapsModel> {
E newEntityCapsModel(String nodeVer);
Single<List<E>> getAllEntityCaps();
Observable<List<E>> getAllEntityCaps();
Single<E> getEntityCapsForNodeVer(String nodeVer);
Completable insertOrReplaceEntityCaps(List<E> entityCaps);
Single<List<Long>> insertOrReplaceEntityCaps(List<E> entityCaps);
Completable insertOrReplaceEntityCaps(E entityCaps);
Single<Long> insertOrReplaceEntityCaps(E entityCaps);
Completable deleteOrReplaceEntityCaps(List<E> entityCaps);

View file

@ -17,6 +17,8 @@ public interface MessageRepository<E extends MessageModel> {
Single<Long> insertMessage(E message);
Single<List<Long>> insertMessages(List<E> messages);
Observable<List<E>> getAllMessages();
Observable<List<E>> getAllMessagesOf(long accountId);

View file

@ -12,7 +12,6 @@ import io.reactivex.Completable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.functions.Consumer;
public abstract class RosterRepository<E extends EntityModel, C extends ContactModel, V extends RosterInformationModel> {
@ -64,7 +63,15 @@ public abstract class RosterRepository<E extends EntityModel, C extends ContactM
* @param contact contact
* @return single wrapping the contacts ID
*/
public abstract Single<Long> updateOrInsertContact(C contact);
public abstract Single<Long> upsertContact(C contact);
/**
* Insert a contact.
*
* @param contact contact
* @return single wrapping the contacts ID
*/
public abstract Single<Long> insertContact(C contact);
/**
* Return the {@link ContactModel} corresponding to the given ID wrapped in a {@link Maybe}.
@ -75,7 +82,7 @@ public abstract class RosterRepository<E extends EntityModel, C extends ContactM
public abstract Maybe<C> getContact(long id);
public Maybe<C> getContact(long accountId, EntityBareJid jid) {
return getEntity(accountId, jid).flatMap(this::getContactForEntity);
return getEntityForAccountAndJid(accountId, jid).flatMap(this::getContactForEntity);
}
/**
@ -223,7 +230,7 @@ public abstract class RosterRepository<E extends EntityModel, C extends ContactM
* @param jid {@link EntityBareJid} of the {@link EntityModel}
* @return
*/
public abstract Maybe<E> getEntity(long accountId, EntityBareJid jid);
public abstract Maybe<E> getEntityForAccountAndJid(long accountId, EntityBareJid jid);
/**
* Return the {@link EntityModel} which has foreign keys pointing to the {@link AccountModel}
@ -233,22 +240,21 @@ public abstract class RosterRepository<E extends EntityModel, C extends ContactM
* @param jid {@link EntityBareJid}
* @return
*/
public Maybe<E> getEntity(AccountModel account, EntityBareJid jid) {
return getEntity(account.getId(), jid);
public Maybe<E> getEntityForAccountAndJid(AccountModel account, EntityBareJid jid) {
return getEntityForAccountAndJid(account.getId(), jid);
}
public abstract Single<E> getEntityForContact(long contactId);
public Single<E> getOrCreateEntityForAccountAndJid(AccountModel accountModel, EntityBareJid jid) {
return getOrCreateEntityForAccountAndJid(accountModel.getId(), jid);
}
public abstract Single<E> getOrCreateEntityForAccountAndJid(long accountId, EntityBareJid jid);
public Single<E> getEntityForContact(C contact) {
return getEntityForContact(contact.getId());
}
/**
* Insert or replace a {@link EntityModel} in the database.
*
* @param entity {@link EntityModel} which we want to insert or replace in the database
* @return {@link Single} which wraps the ID assigned to the entity
*/
public abstract Single<Long> insertOrReplaceEntity(E entity);
}