From 2f454ec05f21ef086646eaa24f4355e291f90fbe Mon Sep 17 00:00:00 2001 From: ChronosX88 Date: Sun, 26 May 2019 11:43:58 +0400 Subject: [PATCH] Implemented Message Archive Management (XEP-313) --- .../2.json | 16 ++- .../3.json | 120 ++++++++++++++++ .../4.json | 120 ++++++++++++++++ .../chronosx88/influence/XMPPConnection.java | 15 +- .../influence/contracts/CoreContracts.kt | 8 +- .../influence/helpers/AppHelper.java | 5 +- .../influence/helpers/LocalDBWrapper.java | 23 ++- .../influence/helpers/NetworkHandler.java | 3 +- .../influence/helpers/RoomHelper.java | 2 +- .../chronosx88/influence/logic/ChatLogic.java | 84 ----------- .../chronosx88/influence/logic/ChatLogic.kt | 136 ++++++++++++++++++ .../influence/models/GenericMessage.java | 6 + .../influence/models/daos/MessageDao.java | 16 ++- .../models/roomEntities/ChatEntity.java | 6 +- .../models/roomEntities/MessageEntity.java | 8 +- .../influence/presenters/ChatPresenter.kt | 67 ++++++++- .../influence/views/ChatActivity.kt | 6 +- .../influence/views/LoginActivity.java | 2 +- 18 files changed, 521 insertions(+), 122 deletions(-) create mode 100644 app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/3.json create mode 100644 app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/4.json delete mode 100644 app/src/main/java/io/github/chronosx88/influence/logic/ChatLogic.java create mode 100644 app/src/main/java/io/github/chronosx88/influence/logic/ChatLogic.kt diff --git a/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/2.json b/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/2.json index 390f447..fae7f53 100644 --- a/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/2.json +++ b/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/2.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "2409c873b47ccd635ed7d10e4d8604f8", + "identityHash": "b15f54b6ea1393c5d45bb222f98ed7d6", "entities": [ { "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `jid` TEXT, `senderJid` TEXT, `timestamp` INTEGER NOT NULL, `text` TEXT, `isSent` INTEGER NOT NULL, `isRead` INTEGER NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chatID` TEXT, `messageUid` TEXT, `senderJid` TEXT, `timestamp` INTEGER NOT NULL, `text` TEXT, `isSent` INTEGER NOT NULL, `isRead` INTEGER NOT NULL)", "fields": [ { "fieldPath": "messageID", @@ -15,8 +15,14 @@ "notNull": true }, { - "fieldPath": "jid", - "columnName": "jid", + "fieldPath": "chatID", + "columnName": "chatID", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "messageUid", + "columnName": "messageUid", "affinity": "TEXT", "notNull": false }, @@ -102,7 +108,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, \"2409c873b47ccd635ed7d10e4d8604f8\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b15f54b6ea1393c5d45bb222f98ed7d6\")" ] } } \ No newline at end of file diff --git a/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/3.json b/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/3.json new file mode 100644 index 0000000..5c8da45 --- /dev/null +++ b/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/3.json @@ -0,0 +1,120 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "b9b5e578f9222b641c833e4ba5e2378c", + "entities": [ + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chatID` TEXT, `messageUid` TEXT, `senderJid` TEXT, `timestamp` INTEGER NOT NULL, `text` TEXT, `isSent` INTEGER NOT NULL, `isRead` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "messageID", + "columnName": "messageID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chatID", + "columnName": "chatID", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "messageUid", + "columnName": "messageUid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "senderJid", + "columnName": "senderJid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSent", + "columnName": "isSent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "isRead", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "messageID" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jid` TEXT NOT NULL, `chatName` TEXT, `users` TEXT, `unreadMessagesCount` INTEGER NOT NULL, `lastMessageUid` TEXT, PRIMARY KEY(`jid`))", + "fields": [ + { + "fieldPath": "jid", + "columnName": "jid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chatName", + "columnName": "chatName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadMessagesCount", + "columnName": "unreadMessagesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessageUid", + "columnName": "lastMessageUid", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "jid" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, \"b9b5e578f9222b641c833e4ba5e2378c\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/4.json b/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/4.json new file mode 100644 index 0000000..12656ec --- /dev/null +++ b/app/schemas/io.github.chronosx88.influence.helpers.RoomHelper/4.json @@ -0,0 +1,120 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "86a2e626209f2c93a3c961821c967d5c", + "entities": [ + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chatID` TEXT, `messageUid` TEXT, `senderJid` TEXT, `timestamp` INTEGER NOT NULL, `text` TEXT, `isSent` INTEGER NOT NULL, `isRead` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "messageID", + "columnName": "messageID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chatID", + "columnName": "chatID", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "messageUid", + "columnName": "messageUid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "senderJid", + "columnName": "senderJid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isSent", + "columnName": "isSent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "isRead", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "messageID" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jid` TEXT NOT NULL, `chatName` TEXT, `users` TEXT, `unreadMessagesCount` INTEGER NOT NULL, `firstMessageUid` TEXT, PRIMARY KEY(`jid`))", + "fields": [ + { + "fieldPath": "jid", + "columnName": "jid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chatName", + "columnName": "chatName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "users", + "columnName": "users", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unreadMessagesCount", + "columnName": "unreadMessagesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstMessageUid", + "columnName": "firstMessageUid", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "jid" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, \"86a2e626209f2c93a3c961821c967d5c\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chronosx88/influence/XMPPConnection.java b/app/src/main/java/io/github/chronosx88/influence/XMPPConnection.java index babdbbd..4c7eda1 100644 --- a/app/src/main/java/io/github/chronosx88/influence/XMPPConnection.java +++ b/app/src/main/java/io/github/chronosx88/influence/XMPPConnection.java @@ -72,7 +72,7 @@ public class XMPPConnection implements ConnectionListener { public XMPPConnection(Context context) { this.prefs = PreferenceManager.getDefaultSharedPreferences(context); this.context = context; - String jid = prefs.getString("jid", null); + String jid = prefs.getString("chatID", null); String password = prefs.getString("pass", null); if(jid != null && password != null) { String username = jid.split("@")[0]; @@ -124,6 +124,8 @@ public class XMPPConnection implements ConnectionListener { try { if(mamManager.isSupported()) { MamManager.getInstanceFor(connection).enableMamForAllMessages(); + } else { + mamManager = null; } } catch (InterruptedException e) { e.printStackTrace(); @@ -170,17 +172,19 @@ public class XMPPConnection implements ConnectionListener { e.printStackTrace(); } - public void sendMessage(EntityBareJid recipientJid, String messageText) { + public String sendMessage(EntityBareJid recipientJid, String messageText) { Chat chat = ChatManager.getInstanceFor(connection).chatWith(recipientJid); try { Message message = new Message(recipientJid, Message.Type.chat); message.setBody(messageText); chat.send(message); + return message.getStanzaId(); } catch (SmackException.NotConnectedException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } + return null; } public XMPPTCPConnection getConnection() { @@ -240,4 +244,11 @@ public class XMPPConnection implements ConnectionListener { } } } + + public MamManager getMamManager() { + if(isConnectionAlive()) { + return mamManager; + } + return null; + } } diff --git a/app/src/main/java/io/github/chronosx88/influence/contracts/CoreContracts.kt b/app/src/main/java/io/github/chronosx88/influence/contracts/CoreContracts.kt index e533ad6..d9be03a 100644 --- a/app/src/main/java/io/github/chronosx88/influence/contracts/CoreContracts.kt +++ b/app/src/main/java/io/github/chronosx88/influence/contracts/CoreContracts.kt @@ -21,12 +21,13 @@ import android.content.Intent import android.view.MenuItem import com.stfalcon.chatkit.dialogs.DialogsListAdapter import com.stfalcon.chatkit.messages.MessagesListAdapter - import io.github.chronosx88.influence.models.GenericDialog import io.github.chronosx88.influence.models.GenericMessage import io.github.chronosx88.influence.models.roomEntities.ChatEntity import io.github.chronosx88.influence.models.roomEntities.MessageEntity +import java9.util.concurrent.CompletableFuture import org.jivesoftware.smack.roster.RosterEntry +import org.jivesoftware.smackx.mam.MamManager interface CoreContracts { @@ -81,11 +82,16 @@ interface CoreContracts { interface IChatLogicContract { fun sendMessage(text: String): MessageEntity? fun getUserStatus(): Boolean + fun loadMessagesFromMAM(): CompletableFuture + fun loadRecentPageMessages(): CompletableFuture + fun loadLocalMessages(): List? } interface IChatPresenterContract { fun sendMessage(text: String): Boolean fun loadLocalMessages() + fun loadMoreMessages() + fun loadRecentPageMessages() fun onDestroy() fun onOptionsItemSelected(item: MenuItem) } diff --git a/app/src/main/java/io/github/chronosx88/influence/helpers/AppHelper.java b/app/src/main/java/io/github/chronosx88/influence/helpers/AppHelper.java index 151876a..976c3d2 100644 --- a/app/src/main/java/io/github/chronosx88/influence/helpers/AppHelper.java +++ b/app/src/main/java/io/github/chronosx88/influence/helpers/AppHelper.java @@ -90,7 +90,7 @@ public class AppHelper extends MultiDexApplication { private static void loadLoginCredentials() { currentLoginCredentials = new LoginCredentials(); - String jid = preferences.getString("jid", null); + String jid = preferences.getString("chatID", null); String password = preferences.getString("pass", null); if(jid != null && password != null) { String username = jid.split("@")[0]; @@ -104,7 +104,7 @@ public class AppHelper extends MultiDexApplication { public static void resetLoginCredentials() { currentLoginCredentials = new LoginCredentials(); - preferences.edit().remove("jid").apply(); + preferences.edit().remove("chatID").apply(); preferences.edit().remove("pass").apply(); } @@ -126,6 +126,7 @@ public class AppHelper extends MultiDexApplication { private void initChatDB() { chatDB = Room.databaseBuilder(getApplicationContext(), RoomHelper.class, "chatDB") + .fallbackToDestructiveMigration() // FIXME ONLY FOR TEST ENVIRONMENT! DON'T USE THIS IN PRODUCTION! .allowMainThreadQueries() .build(); } diff --git a/app/src/main/java/io/github/chronosx88/influence/helpers/LocalDBWrapper.java b/app/src/main/java/io/github/chronosx88/influence/helpers/LocalDBWrapper.java index 4706104..fe6aa1c 100644 --- a/app/src/main/java/io/github/chronosx88/influence/helpers/LocalDBWrapper.java +++ b/app/src/main/java/io/github/chronosx88/influence/helpers/LocalDBWrapper.java @@ -29,16 +29,16 @@ public class LocalDBWrapper { private static RoomHelper dbInstance = AppHelper.getChatDB(); public static void createChatEntry(String jid, String chatName) { - dbInstance.chatDao().addChat(new ChatEntity(jid, chatName, new ArrayList<>(), 0)); + dbInstance.chatDao().addChat(new ChatEntity(jid, chatName, new ArrayList<>(), 0, "")); } - public static long createMessageEntry(String jid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) { - List chatEntities = AppHelper.getChatDB().chatDao().getChatByChatID(jid); + public static long createMessageEntry(String chatID, String messageUid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) { + List chatEntities = AppHelper.getChatDB().chatDao().getChatByChatID(chatID); if(chatEntities.size() < 1) { - Log.e(LOG_TAG, "Failed to create message entry because chat " + jid + " doesn't exists!"); + Log.e(LOG_TAG, "Failed to create message entry because chat " + chatID + " doesn't exists!"); return -1; } - MessageEntity message = new MessageEntity(jid, senderJid, timestamp, text, isSent, isRead); + MessageEntity message = new MessageEntity(chatID, messageUid, senderJid, timestamp, text, isSent, isRead); long index = dbInstance.messageDao().insertMessage(message); return index; } @@ -51,6 +51,14 @@ public class LocalDBWrapper { return messages.get(0); } + public static MessageEntity getMessageByUID(String messageUID) { + List messages = dbInstance.messageDao().getMessageByUID(messageUID); + if(messages.isEmpty()) { + return null; + } + return messages.get(0); + } + public static List getMessagesByChatID(String chatID) { List messages = dbInstance.messageDao().getMessagesByChatID(chatID); if(messages.isEmpty()) { @@ -85,6 +93,11 @@ public class LocalDBWrapper { return getMessageByID(messageID); } + public static MessageEntity getFirstMessage(String chatID) { + long messageID = dbInstance.messageDao().getFirstMessageByChatID(chatID); + return getMessageByID(messageID); + } + public static void updateChatUnreadMessagesCount(String chatID, int unreadMessagesCount) { dbInstance.chatDao().updateUnreadMessagesCount(chatID, unreadMessagesCount); } diff --git a/app/src/main/java/io/github/chronosx88/influence/helpers/NetworkHandler.java b/app/src/main/java/io/github/chronosx88/influence/helpers/NetworkHandler.java index a7d1698..c6020b3 100644 --- a/app/src/main/java/io/github/chronosx88/influence/helpers/NetworkHandler.java +++ b/app/src/main/java/io/github/chronosx88/influence/helpers/NetworkHandler.java @@ -19,7 +19,6 @@ package io.github.chronosx88.influence.helpers; import com.instacart.library.truetime.TrueTime; import org.greenrobot.eventbus.EventBus; -import org.jivesoftware.smack.PresenceListener; import org.jivesoftware.smack.chat2.Chat; import org.jivesoftware.smack.chat2.IncomingChatMessageListener; import org.jivesoftware.smack.packet.Message; @@ -44,7 +43,7 @@ public class NetworkHandler implements IncomingChatMessageListener, PresenceEven if(LocalDBWrapper.getChatByChatID(from.asEntityBareJidString()) == null) { LocalDBWrapper.createChatEntry(chatID, chat.getXmppAddressOfChatPartner().asBareJid().asUnescapedString().split("@")[0]); } - long messageID = LocalDBWrapper.createMessageEntry(chatID, from.asUnescapedString(), TrueTime.now().getTime(), message.getBody(), true, false); + long messageID = LocalDBWrapper.createMessageEntry(chatID, message.getStanzaId(), from.asUnescapedString(), TrueTime.now().getTime(), message.getBody(), true, false); int newUnreadMessagesCount = LocalDBWrapper.getChatByChatID(chatID).unreadMessagesCount + 1; LocalDBWrapper.updateChatUnreadMessagesCount(chatID, newUnreadMessagesCount); diff --git a/app/src/main/java/io/github/chronosx88/influence/helpers/RoomHelper.java b/app/src/main/java/io/github/chronosx88/influence/helpers/RoomHelper.java index 15558f4..54803ab 100644 --- a/app/src/main/java/io/github/chronosx88/influence/helpers/RoomHelper.java +++ b/app/src/main/java/io/github/chronosx88/influence/helpers/RoomHelper.java @@ -24,7 +24,7 @@ import io.github.chronosx88.influence.models.daos.MessageDao; import io.github.chronosx88.influence.models.roomEntities.ChatEntity; import io.github.chronosx88.influence.models.roomEntities.MessageEntity; -@Database(entities = { MessageEntity.class, ChatEntity.class }, version = 2) +@Database(entities = { MessageEntity.class, ChatEntity.class }, version = 4) @TypeConverters({RoomTypeConverter.class}) public abstract class RoomHelper extends RoomDatabase { diff --git a/app/src/main/java/io/github/chronosx88/influence/logic/ChatLogic.java b/app/src/main/java/io/github/chronosx88/influence/logic/ChatLogic.java deleted file mode 100644 index 07f28be..0000000 --- a/app/src/main/java/io/github/chronosx88/influence/logic/ChatLogic.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2019 ChronosX88 - * - * 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 io.github.chronosx88.influence.logic; - -import com.instacart.library.truetime.TrueTime; - -import org.jivesoftware.smack.packet.Presence; -import org.jxmpp.jid.EntityBareJid; -import org.jxmpp.jid.impl.JidCreate; -import org.jxmpp.stringprep.XmppStringprepException; - -import java.io.IOException; - -import io.github.chronosx88.influence.contracts.CoreContracts; -import io.github.chronosx88.influence.helpers.AppHelper; -import io.github.chronosx88.influence.helpers.LocalDBWrapper; -import io.github.chronosx88.influence.models.roomEntities.ChatEntity; -import io.github.chronosx88.influence.models.roomEntities.MessageEntity; - -public class ChatLogic implements CoreContracts.IChatLogicContract { - private String chatID; - private ChatEntity chatEntity; - - public ChatLogic(ChatEntity chatEntity) { - this.chatEntity = chatEntity; - this.chatID = chatEntity.jid; - } - - @Override - public MessageEntity sendMessage(String text) { - if (AppHelper.getXmppConnection().isConnectionAlive()) { - EntityBareJid jid; - try { - jid = JidCreate.entityBareFrom(chatEntity.jid); - } catch (XmppStringprepException e) { - return null; - } - AppHelper.getXmppConnection().sendMessage(jid, text); - while (!TrueTime.isInitialized()) { - new Thread(() -> { - try { - TrueTime.build().initialize(); - } catch (IOException e) { - e.printStackTrace(); - } - }).start(); - } - long messageID = LocalDBWrapper.createMessageEntry(chatID, AppHelper.getJid(), TrueTime.now().getTime(), text, true, false); - return LocalDBWrapper.getMessageByID(messageID); - } else { - return null; - } - } - - @Override - public boolean getUserStatus() { - if(AppHelper.getXmppConnection() != null) { - if(AppHelper.getXmppConnection().isConnectionAlive()) { - Presence presence = null; - try { - presence = AppHelper.getXmppConnection().getUserPresence(JidCreate.bareFrom(chatID)); - } catch (XmppStringprepException e) { - e.printStackTrace(); - } - return presence.isAvailable(); - } - } - return false; - } -} diff --git a/app/src/main/java/io/github/chronosx88/influence/logic/ChatLogic.kt b/app/src/main/java/io/github/chronosx88/influence/logic/ChatLogic.kt new file mode 100644 index 0000000..225af04 --- /dev/null +++ b/app/src/main/java/io/github/chronosx88/influence/logic/ChatLogic.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2019 ChronosX88 + * + * 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 io.github.chronosx88.influence.logic + +import com.instacart.library.truetime.TrueTime +import org.jivesoftware.smack.packet.Presence +import org.jxmpp.jid.EntityBareJid +import org.jxmpp.jid.impl.JidCreate +import org.jxmpp.stringprep.XmppStringprepException + +import java.io.IOException + +import io.github.chronosx88.influence.contracts.CoreContracts +import io.github.chronosx88.influence.helpers.AppHelper +import io.github.chronosx88.influence.helpers.LocalDBWrapper +import io.github.chronosx88.influence.models.roomEntities.ChatEntity +import io.github.chronosx88.influence.models.roomEntities.MessageEntity +import java9.util.concurrent.CompletableFuture +import org.jivesoftware.smackx.mam.MamManager + +class ChatLogic(private val chatEntity: ChatEntity) : CoreContracts.IChatLogicContract { + private val chatID: String + private var mamManager: MamManager? = null + + init { + this.chatID = chatEntity.jid + } + + override fun sendMessage(text: String): MessageEntity? { + if (AppHelper.getXmppConnection().isConnectionAlive) { + val jid: EntityBareJid + try { + jid = JidCreate.entityBareFrom(chatEntity.jid) + } catch (e: XmppStringprepException) { + return null + } + + val messageUid = AppHelper.getXmppConnection().sendMessage(jid, text) + while (!TrueTime.isInitialized()) { + Thread { + try { + TrueTime.build().initialize() + } catch (e: IOException) { + e.printStackTrace() + } + }.start() + } + + var timestamp: Long + try { + timestamp = TrueTime.now().time + } catch (e: Exception) { + // Fallback to Plain Old Java CurrentTimeMillis + timestamp = System.currentTimeMillis() + } + + val messageID = LocalDBWrapper.createMessageEntry(chatID, messageUid, AppHelper.getJid(), timestamp, text, true, false) + return LocalDBWrapper.getMessageByID(messageID) + } else { + return null + } + } + + override fun getUserStatus(): Boolean { + if (AppHelper.getXmppConnection() != null) { + if (AppHelper.getXmppConnection().isConnectionAlive) { + var presence: Presence? = null + try { + presence = AppHelper.getXmppConnection().getUserPresence(JidCreate.bareFrom(chatID)) + } catch (e: XmppStringprepException) { + e.printStackTrace() + } + + return presence!!.isAvailable + } + } + return false + } + + override fun loadMessagesFromMAM(): CompletableFuture { + return CompletableFuture.supplyAsync { + if(AppHelper.getXmppConnection() != null) { + val mamManager: MamManager? = AppHelper.getXmppConnection().mamManager + if(mamManager != null) { + val firstMessageUid = LocalDBWrapper.getChatByChatID(chatID).firstMessageUid + if(firstMessageUid != "") { + return@supplyAsync mamManager.queryArchive(MamManager.MamQueryArgs.builder() + .beforeUid(firstMessageUid) + .limitResultsToJid(JidCreate.from(chatID)) + .setResultPageSizeTo(50) + .build()) + } else { + return@supplyAsync null + } + } else { + return@supplyAsync null + } + } else { + return@supplyAsync null + } + } + } + + override fun loadLocalMessages(): List? { + return LocalDBWrapper.getMessagesByChatID(chatID) + } + + override fun loadRecentPageMessages(): CompletableFuture { + return CompletableFuture.supplyAsync { + if(AppHelper.getXmppConnection() != null) { + val mamManager: MamManager? = AppHelper.getXmppConnection().mamManager + if(mamManager != null) { + return@supplyAsync mamManager.queryMostRecentPage(JidCreate.from(chatID), 20) + } else { + return@supplyAsync null + } + } else { + return@supplyAsync null + } + } + } +} diff --git a/app/src/main/java/io/github/chronosx88/influence/models/GenericMessage.java b/app/src/main/java/io/github/chronosx88/influence/models/GenericMessage.java index 22a08f0..0509958 100644 --- a/app/src/main/java/io/github/chronosx88/influence/models/GenericMessage.java +++ b/app/src/main/java/io/github/chronosx88/influence/models/GenericMessage.java @@ -25,12 +25,14 @@ import io.github.chronosx88.influence.models.roomEntities.MessageEntity; public class GenericMessage implements IMessage { private long messageID; + private String messageUid; private IUser author; private long timestamp; private String text; public GenericMessage(MessageEntity messageEntity) { this.messageID = messageEntity.messageID; + this.messageUid = messageEntity.messageUid; this.author = new GenericUser(messageEntity.senderJid, messageEntity.senderJid, messageEntity.senderJid); this.timestamp = messageEntity.timestamp; this.text = messageEntity.text; @@ -55,4 +57,8 @@ public class GenericMessage implements IMessage { public Date getCreatedAt() { return new Date(timestamp); } + + public String getMessageUid() { + return messageUid; + } } diff --git a/app/src/main/java/io/github/chronosx88/influence/models/daos/MessageDao.java b/app/src/main/java/io/github/chronosx88/influence/models/daos/MessageDao.java index b83e8cd..7703adf 100644 --- a/app/src/main/java/io/github/chronosx88/influence/models/daos/MessageDao.java +++ b/app/src/main/java/io/github/chronosx88/influence/models/daos/MessageDao.java @@ -27,16 +27,16 @@ import io.github.chronosx88.influence.models.roomEntities.MessageEntity; @Dao public interface MessageDao { - @Insert(onConflict = OnConflictStrategy.IGNORE) + @Insert long insertMessage(MessageEntity chatModel); @Query("DELETE FROM messages WHERE messageID = :messageID") void deleteMessage(String messageID); - @Query("DELETE FROM messages WHERE jid = :jid") + @Query("DELETE FROM messages WHERE chatID = :jid") void deleteMessagesByChatID(String jid); - @Query("SELECT * FROM messages WHERE jid = :jid") + @Query("SELECT * FROM messages WHERE chatID = :jid") List getMessagesByChatID(String jid); @Query("SELECT * FROM messages WHERE messageID = :messageID") @@ -48,9 +48,15 @@ public interface MessageDao { @Query("DELETE FROM messages") void clearMessages(); - @Query("DELETE FROM messages WHERE jid = :chatID") + @Query("DELETE FROM messages WHERE chatID = :chatID") void clearMessagesByChatID(String chatID); - @Query("SELECT messageID FROM messages WHERE jid = :chatID GROUP BY :chatID HAVING MAX(messageID)") + @Query("SELECT messageID FROM messages WHERE chatID = :chatID GROUP BY :chatID HAVING MAX(timestamp)") long getLastMessageByChatID(String chatID); + + @Query("SELECT messageID FROM messages WHERE chatID = :chatID GROUP BY :chatID HAVING MIN(timestamp)") + long getFirstMessageByChatID(String chatID); + + @Query("SELECT * FROM messages WHERE messageUid = :uid") + List getMessageByUID(String uid); } diff --git a/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/ChatEntity.java b/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/ChatEntity.java index e11feb5..85293c8 100644 --- a/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/ChatEntity.java +++ b/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/ChatEntity.java @@ -31,15 +31,17 @@ public class ChatEntity { @ColumnInfo public String chatName; @ColumnInfo public ArrayList users; @ColumnInfo public int unreadMessagesCount; + @ColumnInfo public String firstMessageUid; - public ChatEntity(@NonNull String jid, String chatName, ArrayList users, int unreadMessagesCount) { + public ChatEntity(@NonNull String jid, String chatName, ArrayList users, int unreadMessagesCount, String firstMessageUid) { this.jid = jid; this.chatName = chatName; this.users = users; this.unreadMessagesCount = unreadMessagesCount; + this.firstMessageUid = firstMessageUid; } public boolean isPrivateChat() { - return users.size() == 1; + return users.size() == 2; } } diff --git a/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/MessageEntity.java b/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/MessageEntity.java index caf8f22..a961598 100644 --- a/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/MessageEntity.java +++ b/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/MessageEntity.java @@ -24,15 +24,17 @@ import androidx.room.PrimaryKey; @Entity(tableName = "messages") public class MessageEntity { @PrimaryKey(autoGenerate = true) public long messageID; // Global message ID - @ColumnInfo public String jid; // Chat ID + @ColumnInfo public String chatID; // Chat ID + @ColumnInfo public String messageUid; @ColumnInfo public String senderJid; @ColumnInfo public long timestamp; // Timestamp @ColumnInfo public String text; // Message text @ColumnInfo public boolean isSent; // Send status indicator @ColumnInfo public boolean isRead; // Message Read Indicator - public MessageEntity(String jid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) { - this.jid = jid; + public MessageEntity(String chatID, String messageUid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) { + this.chatID = chatID; + this.messageUid = messageUid; this.senderJid = senderJid; this.timestamp = timestamp; this.text = text; diff --git a/app/src/main/java/io/github/chronosx88/influence/presenters/ChatPresenter.kt b/app/src/main/java/io/github/chronosx88/influence/presenters/ChatPresenter.kt index c063883..f7c89c5 100644 --- a/app/src/main/java/io/github/chronosx88/influence/presenters/ChatPresenter.kt +++ b/app/src/main/java/io/github/chronosx88/influence/presenters/ChatPresenter.kt @@ -33,18 +33,21 @@ import io.github.chronosx88.influence.models.appEvents.UserPresenceChangedEvent import io.github.chronosx88.influence.models.roomEntities.ChatEntity import io.github.chronosx88.influence.models.roomEntities.MessageEntity import java9.util.concurrent.CompletableFuture -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async +import java9.util.stream.StreamSupport import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import kotlin.math.log +import org.jivesoftware.smackx.forward.packet.Forwarded +import java.util.* +import kotlin.Comparator +import kotlin.collections.ArrayList class ChatPresenter(private val view: CoreContracts.IChatViewContract, private val chatID: String) : CoreContracts.IChatPresenterContract { private val logic: CoreContracts.IChatLogicContract private val chatEntity: ChatEntity? private val gson: Gson private val chatAdapter: MessagesListAdapter + private val messageComparator = Comparator { o1, o2 -> o1.createdAt.time.compareTo(o2.createdAt.time) } init { this.logic = ChatLogic(LocalDBWrapper.getChatByChatID(chatID)!!) @@ -53,6 +56,7 @@ class ChatPresenter(private val view: CoreContracts.IChatViewContract, private v val holdersConfig = MessageHolders() holdersConfig.setIncomingTextLayout(R.layout.item_incoming_text_message_custom) chatAdapter = MessagesListAdapter(AppHelper.getJid(), holdersConfig, AvatarImageLoader()) + chatAdapter.setLoadMoreListener { page, _ -> loadMoreMessages() } view.setAdapter(chatAdapter) getUserStatus() EventBus.getDefault().register(this) @@ -70,13 +74,14 @@ class ChatPresenter(private val view: CoreContracts.IChatViewContract, private v } override fun loadLocalMessages() { - val entities: List? = LocalDBWrapper.getMessagesByChatID(chatID) + val entities = logic.loadLocalMessages() val messages = ArrayList() if(entities != null) { entities.forEach { messages.add(GenericMessage(it)) } } + messages.sortWith(messageComparator) chatAdapter.addToEnd(messages, true) } @@ -123,4 +128,58 @@ class ChatPresenter(private val view: CoreContracts.IChatViewContract, private v } } } + + override fun loadMoreMessages() { + logic.loadMessagesFromMAM().thenAccept { query -> + if(query != null) { + val adapterMessages = ArrayList() + StreamSupport.stream(query.page.forwarded) + .forEach { forwardedMessage -> + val message = Forwarded.extractMessagesFrom(Collections.singleton(forwardedMessage))[0] + if(message.body != null) { + if(LocalDBWrapper.getMessageByUID(message.stanzaId) == null) { + val messageID = LocalDBWrapper.createMessageEntry(chatID, message.stanzaId, message.from.asBareJid().asUnescapedString(), forwardedMessage.delayInformation.stamp.time, message.body, true, true) + adapterMessages.add(GenericMessage(LocalDBWrapper.getMessageByID(messageID))) + } + } + } + AppHelper.getMainUIThread().post { + adapterMessages.sortWith(messageComparator) + chatAdapter.addToEnd(adapterMessages, true) + } + if(query.messageCount != 0) { + chatEntity!!.firstMessageUid = query.mamResultExtensions[0].id + LocalDBWrapper.updateChatEntity(chatEntity) + } + } + } + } + + override fun loadRecentPageMessages() { + logic.loadRecentPageMessages().thenAccept { query -> + if(query != null) { + val adapterMessages = ArrayList() + StreamSupport.stream(query.page.forwarded) + .forEach { forwardedMessage -> + val message = Forwarded.extractMessagesFrom(Collections.singleton(forwardedMessage))[0] + if(message.body != null) { + if(LocalDBWrapper.getMessageByUID(message.stanzaId) == null) { + val messageID = LocalDBWrapper.createMessageEntry(chatID, message.stanzaId, message.from.asBareJid().asUnescapedString(), forwardedMessage.delayInformation.stamp.time, message.body, true, true) + adapterMessages.add(GenericMessage(LocalDBWrapper.getMessageByID(messageID))) + } + } + } + AppHelper.getMainUIThread().post { + adapterMessages.sortWith(messageComparator) + adapterMessages.forEach { + chatAdapter.addToStart(it, true) + } + } + if(query.messageCount != 0 && chatEntity!!.firstMessageUid == "") { + chatEntity.firstMessageUid = query.mamResultExtensions[0].id + LocalDBWrapper.updateChatEntity(chatEntity) + } + } + } + } } diff --git a/app/src/main/java/io/github/chronosx88/influence/views/ChatActivity.kt b/app/src/main/java/io/github/chronosx88/influence/views/ChatActivity.kt index 44030a7..053d170 100644 --- a/app/src/main/java/io/github/chronosx88/influence/views/ChatActivity.kt +++ b/app/src/main/java/io/github/chronosx88/influence/views/ChatActivity.kt @@ -28,18 +28,13 @@ import androidx.appcompat.widget.Toolbar import androidx.recyclerview.widget.LinearLayoutManager import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.util.ColorGenerator -import com.stfalcon.chatkit.commons.ImageLoader import com.stfalcon.chatkit.messages.MessageInput import com.stfalcon.chatkit.messages.MessagesList import com.stfalcon.chatkit.messages.MessagesListAdapter import io.github.chronosx88.influence.R import io.github.chronosx88.influence.contracts.CoreContracts -import io.github.chronosx88.influence.helpers.AppHelper -import io.github.chronosx88.influence.helpers.LocalDBWrapper import io.github.chronosx88.influence.models.GenericMessage -import io.github.chronosx88.influence.models.roomEntities.MessageEntity import io.github.chronosx88.influence.presenters.ChatPresenter -import kotlinx.android.synthetic.main.activity_chat.view.* import org.jetbrains.anko.find class ChatActivity : AppCompatActivity(), CoreContracts.IChatViewContract { @@ -74,6 +69,7 @@ class ChatActivity : AppCompatActivity(), CoreContracts.IChatViewContract { presenter = ChatPresenter(this, intent.getStringExtra("chatID")) loadAvatarFromIntent(intent) presenter!!.loadLocalMessages() + presenter!!.loadRecentPageMessages() } override fun onOptionsItemSelected(item: MenuItem): Boolean { diff --git a/app/src/main/java/io/github/chronosx88/influence/views/LoginActivity.java b/app/src/main/java/io/github/chronosx88/influence/views/LoginActivity.java index 28adf4b..962203a 100644 --- a/app/src/main/java/io/github/chronosx88/influence/views/LoginActivity.java +++ b/app/src/main/java/io/github/chronosx88/influence/views/LoginActivity.java @@ -131,7 +131,7 @@ public class LoginActivity extends AppCompatActivity implements CoreContracts.IL private void saveLoginCredentials() { AppHelper.getPreferences().edit() - .putString("jid", jidEditText.getText().toString()) + .putString("chatID", jidEditText.getText().toString()) .putString("pass", passwordEditText.getText().toString()) .putBoolean("logged_in", true) .apply();