diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 0c98b6eb4ee5e1d45b2e35a2259aba5bc0ed7b27..861b904b8f2b2d9bf50f081f5c2bd376f6fad006 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -48,7 +48,7 @@ public class PgpEngine { public void encrypt(final Message message, final UiCallback callback) { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_ENCRYPT); - final Conversation conversation = message.getConversation(); + final Conversation conversation = (Conversation) message.getConversation(); if (conversation.getMode() == Conversation.MODE_SINGLE) { long[] keys = { conversation.getContact().getPgpKeyId(), diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index e8a47e447e3378183fffbc2f9efb38129b620b4e..6e3d1756c1bf16e5fe747f52fab94fafc5e6b09b 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1307,7 +1307,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (message.getType() == Message.TYPE_PRIVATE) { success = buildHeader(axolotlMessage, message.getTrueCounterpart()); } else { - success = buildHeader(axolotlMessage, message.getConversation()); + success = buildHeader(axolotlMessage, (Conversation) message.getConversation()); } return success ? axolotlMessage : null; } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index ff39ba611e987c7e33d2cfeeb11d5060e101dc5c..4c689ab740ea683b4332e13ab99bd5bc455a6b97 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -30,15 +30,12 @@ import rocks.xmpp.addr.Jid; import static eu.siacs.conversations.entities.Bookmark.printableValue; -public class Conversation extends AbstractEntity implements Blockable, Comparable { +public class Conversation extends AbstractEntity implements Blockable, Comparable, Conversational { public static final String TABLENAME = "conversations"; public static final int STATUS_AVAILABLE = 0; public static final int STATUS_ARCHIVED = 1; - public static final int MODE_MULTI = 1; - public static final int MODE_SINGLE = 0; - public static final String NAME = "name"; public static final String ACCOUNT = "accountUuid"; public static final String CONTACT = "contactUuid"; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversational.java b/src/main/java/eu/siacs/conversations/entities/Conversational.java new file mode 100644 index 0000000000000000000000000000000000000000..6b016230dfc1c46818fdeacf449e7dcfede84e98 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Conversational.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.siacs.conversations.entities; + +import rocks.xmpp.addr.Jid; + +public interface Conversational { + + int MODE_MULTI = 1; + int MODE_SINGLE = 0; + + Account getAccount(); + + Contact getContact(); + + Jid getJid(); + + int getMode(); + + String getUuid(); +} diff --git a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..305fd67df0cbdd73dc212df9bffdb3d4d7f6906b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.siacs.conversations.entities; + +import android.database.Cursor; + +import java.util.Set; + +import rocks.xmpp.addr.Jid; + +public class IndividualMessage extends Message { + + + private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, String edited, boolean oob, String errorMessage, Set readByMarkers, boolean markable) { + super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable); + } + + @Override + public Message next() { + return null; + } + + @Override + public Message prev() { + return null; + } + + @Override + public boolean isValidInSession() { + return true; + } + + public static Message fromCursor(Cursor cursor, Conversational conversation) { + Jid jid; + try { + String value = cursor.getString(cursor.getColumnIndex(COUNTERPART)); + if (value != null) { + jid = Jid.of(value); + } else { + jid = null; + } + } catch (IllegalArgumentException e) { + jid = null; + } catch (IllegalStateException e) { + return null; // message too long? + } + Jid trueCounterpart; + try { + String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)); + if (value != null) { + trueCounterpart = Jid.of(value); + } else { + trueCounterpart = null; + } + } catch (IllegalArgumentException e) { + trueCounterpart = null; + } + return new IndividualMessage(conversation, + cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(CONVERSATION)), + jid, + trueCounterpart, + cursor.getString(cursor.getColumnIndex(BODY)), + cursor.getLong(cursor.getColumnIndex(TIME_SENT)), + cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(TYPE)), + cursor.getInt(cursor.getColumnIndex(CARBON)) > 0, + cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), + cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), + cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), + cursor.getString(cursor.getColumnIndex(FINGERPRINT)), + cursor.getInt(cursor.getColumnIndex(READ)) > 0, + cursor.getString(cursor.getColumnIndex(EDITED)), + cursor.getInt(cursor.getColumnIndex(OOB)) > 0, + cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)), + ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), + cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0); + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index cfc61ee690bc0dc18a2df06cfc87c2de2c38f514..1bab78b7aa106e3bf798996b2ddf263683223e87 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -90,7 +90,7 @@ public class Message extends AbstractEntity { protected boolean read = true; protected String remoteMsgId = null; protected String serverMsgId = null; - private final Conversation conversation; + private final Conversational conversation; protected Transferable transferable = null; private Message mNextMessage = null; private Message mPreviousMessage = null; @@ -105,15 +105,15 @@ public class Message extends AbstractEntity { private List counterparts; private WeakReference user; - private Message(Conversation conversation) { + private Message(Conversational conversation) { this.conversation = conversation; } - public Message(Conversation conversation, String body, int encryption) { + public Message(Conversational conversation, String body, int encryption) { this(conversation, body, encryption, STATUS_UNSEND); } - public Message(Conversation conversation, String body, int encryption, int status) { + public Message(Conversational conversation, String body, int encryption, int status) { this(conversation, java.util.UUID.randomUUID().toString(), conversation.getUuid(), conversation.getJid() == null ? null : conversation.getJid().asBareJid(), @@ -136,12 +136,12 @@ public class Message extends AbstractEntity { false); } - private Message(final Conversation conversation, final String uuid, final String conversationUUid, final Jid counterpart, - final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final boolean carbon, - final String remoteMsgId, final String relativeFilePath, - final String serverMsgId, final String fingerprint, final boolean read, - final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, + protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, + final Jid trueCounterpart, final String body, final long timeSent, + final int encryption, final int status, final int type, final boolean carbon, + final String remoteMsgId, final String relativeFilePath, + final String serverMsgId, final String fingerprint, final boolean read, + final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, final boolean markable) { this.conversation = conversation; this.uuid = uuid; @@ -252,7 +252,7 @@ public class Message extends AbstractEntity { } else { values.put(TRUE_COUNTERPART, trueCounterpart.toString()); } - values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0,Config.MAX_STORAGE_MESSAGE_CHARS) : body); + values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body); values.put(TIME_SENT, timeSent); values.put(ENCRYPTION, encryption); values.put(STATUS, status); @@ -262,11 +262,11 @@ public class Message extends AbstractEntity { values.put(RELATIVE_FILE_PATH, relativeFilePath); values.put(SERVER_MSG_ID, serverMsgId); values.put(FINGERPRINT, axolotlFingerprint); - values.put(READ,read ? 1 : 0); + values.put(READ, read ? 1 : 0); values.put(EDITED, edited); values.put(OOB, oob ? 1 : 0); - values.put(ERROR_MESSAGE,errorMessage); - values.put(READ_BY_MARKERS,ReadByMarker.toJson(readByMarkers).toString()); + values.put(ERROR_MESSAGE, errorMessage); + values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString()); values.put(MARKABLE, markable ? 1 : 0); return values; } @@ -275,7 +275,7 @@ public class Message extends AbstractEntity { return conversationUuid; } - public Conversation getConversation() { + public Conversational getConversation() { return this.conversation; } @@ -497,7 +497,7 @@ public class Message extends AbstractEntity { } return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) && matchingCounterpart - && (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); + && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); } else { return this.remoteMsgId == null && matchingCounterpart @@ -508,36 +508,46 @@ public class Message extends AbstractEntity { } public Message next() { - synchronized (this.conversation.messages) { - if (this.mNextMessage == null) { - int index = this.conversation.messages.indexOf(this); - if (index < 0 || index >= this.conversation.messages.size() - 1) { - this.mNextMessage = null; - } else { - this.mNextMessage = this.conversation.messages.get(index + 1); + if (this.conversation instanceof Conversation) { + final Conversation conversation = (Conversation) this.conversation; + synchronized (conversation.messages) { + if (this.mNextMessage == null) { + int index = conversation.messages.indexOf(this); + if (index < 0 || index >= conversation.messages.size() - 1) { + this.mNextMessage = null; + } else { + this.mNextMessage = conversation.messages.get(index + 1); + } } + return this.mNextMessage; } - return this.mNextMessage; + } else { + throw new AssertionError("Calling next should be disabled for stubs"); } } public Message prev() { - synchronized (this.conversation.messages) { - if (this.mPreviousMessage == null) { - int index = this.conversation.messages.indexOf(this); - if (index <= 0 || index > this.conversation.messages.size()) { - this.mPreviousMessage = null; - } else { - this.mPreviousMessage = this.conversation.messages.get(index - 1); + if (this.conversation instanceof Conversation) { + final Conversation conversation = (Conversation) this.conversation; + synchronized (conversation.messages) { + if (this.mPreviousMessage == null) { + int index = conversation.messages.indexOf(this); + if (index <= 0 || index > conversation.messages.size()) { + this.mPreviousMessage = null; + } else { + this.mPreviousMessage = conversation.messages.get(index - 1); + } } } return this.mPreviousMessage; + } else { + throw new AssertionError("Calling prev should be disabled for stubs"); } } public boolean isLastCorrectableMessage() { Message next = next(); - while(next != null) { + while (next != null) { if (next.isCorrectable()) { return false; } @@ -566,7 +576,7 @@ public class Message extends AbstractEntity { this.edited() == message.edited() && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS && - !message.isGeoUri()&& + !message.isGeoUri() && !this.isGeoUri() && !message.treatAsDownloadable() && !this.treatAsDownloadable() && @@ -575,7 +585,7 @@ public class Message extends AbstractEntity { !this.bodyIsOnlyEmojis() && !message.bodyIsOnlyEmojis() && ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && - UIHelper.sameDay(message.getTimeSent(),this.getTimeSent()) && + UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) && this.getReadByMarkers().equals(message.getReadByMarkers()) && !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS) ); @@ -599,7 +609,8 @@ public class Message extends AbstractEntity { return this.counterparts; } - public static class MergeSeparator {} + public static class MergeSeparator { + } public SpannableStringBuilder getMergedBody() { SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim()); @@ -624,7 +635,7 @@ public class Message extends AbstractEntity { public int getMergedStatus() { int status = this.status; Message current = this; - while(current.mergeable(current.next())) { + while (current.mergeable(current.next())) { current = current.next(); if (current == null) { break; @@ -637,7 +648,7 @@ public class Message extends AbstractEntity { public long getMergedTimeSent() { long time = this.timeSent; Message current = this; - while(current.mergeable(current.next())) { + while (current.mergeable(current.next())) { current = current.next(); if (current == null) { break; @@ -708,11 +719,11 @@ public class Message extends AbstractEntity { if (treatAsDownloadable == null) { try { final String[] lines = body.split("\n"); - if (lines.length ==0) { + if (lines.length == 0) { treatAsDownloadable = false; return false; } - for(String line : lines) { + for (String line : lines) { if (line.contains("\\s+")) { treatAsDownloadable = false; return false; @@ -735,7 +746,7 @@ public class Message extends AbstractEntity { public synchronized boolean bodyIsOnlyEmojis() { if (isEmojisOnly == null) { - isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s","")); + isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", "")); } return isEmojisOnly; } @@ -847,9 +858,9 @@ public class Message extends AbstractEntity { return s != null && s.isTrusted(); } - private int getPreviousEncryption() { - for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){ - if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { + private int getPreviousEncryption() { + for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) { + if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { continue; } return iterator.getEncryption(); @@ -858,13 +869,18 @@ public class Message extends AbstractEntity { } private int getNextEncryption() { - for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){ - if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { - continue; + if (this.conversation instanceof Conversation) { + Conversation conversation = (Conversation) this.conversation; + for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) { + if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { + continue; + } + return iterator.getEncryption(); } - return iterator.getEncryption(); + return conversation.getNextEncryption(); + } else { + throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs"); } - return conversation.getNextEncryption(); } public boolean isValidInSession() { diff --git a/src/main/java/eu/siacs/conversations/entities/StubConversation.java b/src/main/java/eu/siacs/conversations/entities/StubConversation.java new file mode 100644 index 0000000000000000000000000000000000000000..d89217599e33a03d0057b193b2881ffb44548195 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/StubConversation.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.siacs.conversations.entities; + +import rocks.xmpp.addr.Jid; + + +public class StubConversation implements Conversational { + + private final Account account; + private final String uuid; + private final Jid jid; + private final int mode; + + public StubConversation(Account account, String uuid, Jid jid, int mode) { + this.account = account; + this.uuid = uuid; + this.jid = jid; + this.mode = mode; + } + + @Override + public Account getAccount() { + return account; + } + + @Override + public Contact getContact() { + return account.getRoster().getContact(jid); + } + + @Override + public Jid getJid() { + return jid; + } + + @Override + public int getMode() { + return mode; + } + + @Override + public String getUuid() { + return uuid; + } +} diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 833d1fb23f9d720f69766fd49e100f40f02e5fe8..7022ba9e8f15ba53c6c7198debd0605122aa7bf1 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -29,7 +29,7 @@ public class MessageGenerator extends AbstractGenerator { } private MessagePacket preparePacket(Message message) { - Conversation conversation = message.getConversation(); + Conversation conversation = (Conversation) message.getConversation(); Account account = conversation.getAccount(); MessagePacket packet = new MessagePacket(); final boolean isWithSelf = conversation.getContact().isSelf(); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 99523e9e8884fd6df09340e7c8b0ce7d3e3293f1..01d7e486baa39e2dc83a7290ad45c45ad15c10c0 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -674,8 +674,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { return getMessages(conversations, limit, -1); } - public ArrayList getMessages(Conversation conversation, int limit, - long timestamp) { + public ArrayList getMessages(Conversation conversation, int limit, long timestamp) { ArrayList list = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor; @@ -705,40 +704,43 @@ public class DatabaseBackend extends SQLiteOpenHelper { return list; } + public Cursor getMessageSearchCursor(String term) { + SQLiteDatabase db = this.getReadableDatabase(); + String SQL = "SELECT "+Message.TABLENAME+".*,"+Conversation.TABLENAME+'.'+Conversation.CONTACTJID+','+Conversation.TABLENAME+'.'+Conversation.ACCOUNT+','+Conversation.TABLENAME+'.'+Conversation.MODE+" FROM "+Message.TABLENAME +" join "+Conversation.TABLENAME+" on "+Message.TABLENAME+'.'+Message.CONVERSATION+'='+Conversation.TABLENAME+'.'+Conversation.UUID+" where "+Message.BODY +" LIKE ? limit 200"; + return db.rawQuery(SQL,new String[]{'%'+term+'%'}); + } + public Iterable getMessagesIterable(final Conversation conversation) { - return new Iterable() { - @Override - public Iterator iterator() { - class MessageIterator implements Iterator { - SQLiteDatabase db = getReadableDatabase(); - String[] selectionArgs = {conversation.getUuid()}; - Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=?", selectionArgs, null, null, Message.TIME_SENT - + " ASC", null); - - public MessageIterator() { - cursor.moveToFirst(); - } + return () -> { + class MessageIterator implements Iterator { + private SQLiteDatabase db = getReadableDatabase(); + private String[] selectionArgs = {conversation.getUuid()}; + private Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=?", selectionArgs, null, null, Message.TIME_SENT + + " ASC", null); + + private MessageIterator() { + cursor.moveToFirst(); + } - @Override - public boolean hasNext() { - return !cursor.isAfterLast(); - } + @Override + public boolean hasNext() { + return !cursor.isAfterLast(); + } - @Override - public Message next() { - Message message = Message.fromCursor(cursor, conversation); - cursor.moveToNext(); - return message; - } + @Override + public Message next() { + Message message = Message.fromCursor(cursor, conversation); + cursor.moveToNext(); + return message; + } - @Override - public void remove() { - throw new UnsupportedOperationException(); - } + @Override + public void remove() { + throw new UnsupportedOperationException(); } - return new MessageIterator(); } + return new MessageIterator(); }; } diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index 80da03f07d66d94853d95535d36c8a61232bb14a..02f351c8b1be934f03a432d809ad48507717ce74 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -28,6 +28,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; @@ -388,20 +389,21 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded { } public Bitmap get(Message message, int size, boolean cachedOnly) { - final Conversation conversation = message.getConversation(); + final Conversational conversation = message.getConversation(); if (message.getType() == Message.TYPE_STATUS && message.getCounterparts() != null && message.getCounterparts().size() > 1) { return get(message.getCounterparts(), size, cachedOnly); } else if (message.getStatus() == Message.STATUS_RECEIVED) { Contact c = message.getContact(); if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) { return get(c, size, cachedOnly); - } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + } else if (conversation instanceof Conversation && message.getConversation().getMode() == Conversation.MODE_MULTI) { final Jid trueCounterpart = message.getTrueCounterpart(); + final MucOptions mucOptions = ((Conversation) conversation).getMucOptions(); MucOptions.User user; if (trueCounterpart != null) { - user = conversation.getMucOptions().findUserByRealJid(trueCounterpart); + user = mucOptions.findUserByRealJid(trueCounterpart); } else { - user = conversation.getMucOptions().findUserByFullJid(message.getCounterpart()); + user = mucOptions.findUserByFullJid(message.getCounterpart()); } if (user != null) { return getImpl(user, size, cachedOnly); diff --git a/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java b/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java new file mode 100644 index 0000000000000000000000000000000000000000..20cdec3511a74f20c8b17169b6e7afcb95ffb758 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.siacs.conversations.services; + +import android.database.Cursor; +import android.os.SystemClock; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; +import eu.siacs.conversations.entities.IndividualMessage; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.StubConversation; +import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; +import eu.siacs.conversations.utils.Cancellable; +import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; +import rocks.xmpp.addr.Jid; + +public class MessageSearchTask implements Runnable, Cancellable { + + private static final ReplacingSerialSingleThreadExecutor EXECUTOR = new ReplacingSerialSingleThreadExecutor(MessageSearchTask.class.getName()); + + private final XmppConnectionService xmppConnectionService; + private final String term; + private final OnSearchResultsAvailable onSearchResultsAvailable; + + private boolean isCancelled = false; + + private MessageSearchTask(XmppConnectionService xmppConnectionService, String term, OnSearchResultsAvailable onSearchResultsAvailable) { + this.xmppConnectionService = xmppConnectionService; + this.term = term; + this.onSearchResultsAvailable = onSearchResultsAvailable; + } + + @Override + public void cancel() { + this.isCancelled = true; + } + + @Override + public void run() { + long startTimestamp = SystemClock.elapsedRealtime(); + Cursor cursor = null; + try { + final HashMap conversationCache = new HashMap<>(); + final List result = new ArrayList<>(); + cursor = xmppConnectionService.databaseBackend.getMessageSearchCursor(term); + while(cursor.moveToNext()) { + final String conversationUuid = cursor.getString(cursor.getColumnIndex(Message.CONVERSATION)); + Conversational conversation; + if (conversationCache.containsKey(conversationUuid)) { + conversation = conversationCache.get(conversationUuid); + } else { + String accountUuid = cursor.getString(cursor.getColumnIndex(Conversation.ACCOUNT)); + String contactJid = cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)); + int mode = cursor.getInt(cursor.getColumnIndex(Conversation.MODE)); + conversation = findOrGenerateStub(conversationUuid, accountUuid, contactJid, mode); + conversationCache.put(conversationUuid, conversation); + } + Message message = IndividualMessage.fromCursor(cursor, conversation); + result.add(message); + } + long stopTimestamp = SystemClock.elapsedRealtime(); + Log.d(Config.LOGTAG,"found "+result.size()+" messages in "+(stopTimestamp - startTimestamp)+"ms"); + onSearchResultsAvailable.onSearchResultsAvailable(term, result); + } catch (Exception e) { + Log.d(Config.LOGTAG,"exception while searching ",e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private Conversational findOrGenerateStub(String conversationUuid, String accountUuid, String contactJid, int mode) throws Exception { + Conversation conversation = xmppConnectionService.findConversationByUuid(conversationUuid); + if (conversation != null) { + return conversation; + } + Account account = xmppConnectionService.findAccountByUuid(accountUuid); + Jid jid = Jid.of(contactJid); + if (account != null && jid != null) { + return new StubConversation(account, conversationUuid, jid.asBareJid(), mode); + } + throw new Exception("Unable to generate stub for "+contactJid); + } + + private void executeInBackground() { + EXECUTOR.execute(this); + } + + public static void search(XmppConnectionService xmppConnectionService, String term, OnSearchResultsAvailable onSearchResultsAvailable) { + new MessageSearchTask(xmppConnectionService, term, onSearchResultsAvailable).executeInBackground(); + } +} diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index c061cae5cce6666d384565657f45887ecdcf5def..21f5e57dfdf3e133e3cfe3ef069e37a8e8114d85 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -42,6 +42,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.ConversationsActivity; @@ -77,11 +78,12 @@ public class NotificationService { } public boolean notify(final Message message) { + final Conversation conversation = (Conversation) message.getConversation(); return message.getStatus() == Message.STATUS_RECEIVED && notificationsEnabled() - && !message.getConversation().isMuted() - && (message.getConversation().alwaysNotify() || wasHighlightedOrPrivate(message)) - && (!message.getConversation().isWithStranger() || notificationsFromStrangers()) + && !conversation.isMuted() + && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message)) + && (!conversation.isWithStranger() || notificationsFromStrangers()) ; } @@ -112,7 +114,7 @@ public class NotificationService { public void pushFromBacklog(final Message message) { if (notify(message)) { synchronized (notifications) { - getBacklogMessageCounter(message.getConversation()).incrementAndGet(); + getBacklogMessageCounter((Conversation) message.getConversation()).incrementAndGet(); pushToStack(message); } } @@ -239,7 +241,7 @@ public class NotificationService { if (messages != null && messages.size() > 0) { Message last = messages.get(messages.size() - 1); if (last.getStatus() != Message.STATUS_RECEIVED) { - if (mXmppConnectionService.markRead(last.getConversation(), false)) { + if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) { mXmppConnectionService.updateConversationUi(); } } @@ -337,7 +339,7 @@ public class NotificationService { Conversation conversation = null; for (final ArrayList messages : notifications.values()) { if (messages.size() > 0) { - conversation = messages.get(0).getConversation(); + conversation = (Conversation) messages.get(0).getConversation(); final String name = conversation.getName().toString(); SpannableString styledString; if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { @@ -376,7 +378,7 @@ public class NotificationService { private Builder buildSingleConversations(final ArrayList messages) { final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); if (messages.size() >= 1) { - final Conversation conversation = messages.get(0).getConversation(); + final Conversation conversation = (Conversation) messages.get(0).getConversation(); final UnreadConversation.Builder mUnreadBuilder = new UnreadConversation.Builder(conversation.getName().toString()); mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService() .get(conversation, getPixel(64))); @@ -510,7 +512,7 @@ public class NotificationService { private void modifyForTextOnly(final Builder builder, final UnreadConversation.Builder uBuilder, final ArrayList messages) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(mXmppConnectionService.getString(R.string.me)); - Conversation conversation = messages.get(0).getConversation(); + final Conversation conversation = (Conversation) messages.get(0).getConversation(); if (conversation.getMode() == Conversation.MODE_MULTI) { messagingStyle.setConversationTitle(conversation.getName()); } @@ -632,7 +634,7 @@ public class NotificationService { return (actionId * NOTIFICATION_ID_MULTIPLIER) + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER); } - private int generateRequestCode(Conversation conversation, int actionId) { + private int generateRequestCode(Conversational conversation, int actionId) { return generateRequestCode(conversation.getUuid(), actionId); } @@ -640,7 +642,7 @@ public class NotificationService { return createContentIntent(message.getConversationUuid(), message.getUuid()); } - private PendingIntent createContentIntent(final Conversation conversation) { + private PendingIntent createContentIntent(final Conversational conversation) { return createContentIntent(conversation.getUuid(), null); } @@ -692,13 +694,18 @@ public class NotificationService { } private boolean wasHighlightedOrPrivate(final Message message) { - final String nick = message.getConversation().getMucOptions().getActualNick(); - final Pattern highlight = generateNickHighlightPattern(nick); - if (message.getBody() == null || nick == null) { + if (message.getConversation() instanceof Conversation) { + Conversation conversation = (Conversation) message.getConversation(); + final String nick = conversation.getMucOptions().getActualNick(); + final Pattern highlight = generateNickHighlightPattern(nick); + if (message.getBody() == null || nick == null) { + return false; + } + final Matcher m = highlight.matcher(message.getBody()); + return (m.find() || message.getType() == Message.TYPE_PRIVATE); + } else { return false; } - final Matcher m = highlight.matcher(message.getBody()); - return (m.find() || message.getType() == Message.TYPE_PRIVATE); } public static Pattern generateNickHighlightPattern(final String nick) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 4770a05f62c0b79a087b069194979e9b14bd1eb0..de1909b74bbb2cfd29a510a7de6466d4b073b5e7 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -98,6 +98,7 @@ import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.UiCallback; +import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; import eu.siacs.conversations.utils.ConversationsFileObserver; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExceptionHelper; @@ -534,6 +535,10 @@ public class XmppConnectionService extends Service { return find(getConversations(), account, jid); } + public void search(String term, OnSearchResultsAvailable onSearchResultsAvailable) { + MessageSearchTask.search(this, term, onSearchResultsAvailable); + } + @Override public int onStartCommand(Intent intent, int flags, int startId) { final String action = intent == null ? null : intent.getAction(); @@ -781,7 +786,7 @@ public class XmppConnectionService extends Service { message.setEncryption(Message.ENCRYPTION_DECRYPTED); sendMessage(message); if (dismissAfterReply) { - markRead(message.getConversation(), true); + markRead((Conversation) message.getConversation(), true); } else { mNotificationService.pushFromDirectReply(message); } @@ -1142,7 +1147,7 @@ public class XmppConnectionService extends Service { databaseBackend.updateAccount(account); mNotificationService.updateErrorNotification(); } - final Conversation conversation = message.getConversation(); + final Conversation conversation = (Conversation) message.getConversation(); account.deactivateGracePeriod(); MessagePacket packet = null; final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI @@ -3242,6 +3247,15 @@ public class XmppConnectionService extends Service { return null; } + public Account findAccountByUuid(final String uuid) { + for(Account account : this.accounts) { + if (account.getUuid().equals(uuid)) { + return account; + } + } + return null; + } + public Conversation findConversationByUuid(String uuid) { for (Conversation conversation : getConversations()) { if (conversation.getUuid().equals(uuid)) { @@ -3528,7 +3542,9 @@ public class XmppConnectionService extends Service { markMessage(msg, Message.STATUS_WAITING); this.resendMessage(msg, false); } - message.getConversation().sort(); + if (message.getConversation() instanceof Conversation) { + ((Conversation) message.getConversation()).sort(); + } updateConversationUi(); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 390d53c8ad53ec1f5859ae3d099f1cfa14efe210..3e127c69019c72e6ef8eba094dc43e4b9f65a740 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -705,7 +705,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke message.setEdited(message.getUuid()); message.setUuid(UUID.randomUUID().toString()); } - switch (message.getConversation().getNextEncryption()) { + switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_PGP: sendPgpMessage(message); break; @@ -932,11 +932,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke messageListAdapter.setOnContactPictureClicked(message -> { final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; if (received) { - if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getConversation() instanceof Conversation && message.getConversation().getMode() == Conversation.MODE_MULTI) { Jid user = message.getCounterpart(); if (user != null && !user.isBareJid()) { - final MucOptions mucOptions = message.getConversation().getMucOptions(); - if (mucOptions.participating() || message.getConversation().getNextCounterpart() != null) { + final MucOptions mucOptions = ((Conversation) message.getConversation()).getMucOptions(); + if (mucOptions.participating() || ((Conversation) message.getConversation()).getNextCounterpart() != null) { if (!mucOptions.isUserInRoom(user)) { Toast.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResource()), Toast.LENGTH_SHORT).show(); } @@ -1079,7 +1079,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } if (relevantForCorrection.getType() == Message.TYPE_TEXT && relevantForCorrection.isLastCorrectableMessage() - && (m.getConversation().getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) { + && m.getConversation() instanceof Conversation + && (((Conversation) m.getConversation()).getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) { correctMessage.setVisible(true); } if ((m.isFileOrImage() && !deleted && !receiving) || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable())) { @@ -1628,9 +1629,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void resendMessage(final Message message) { if (message.isFileOrImage()) { + if (!(message.getConversation() instanceof Conversation)) { + return; + } + final Conversation conversation = (Conversation) message.getConversation(); DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); if (file.exists()) { - final Conversation conversation = message.getConversation(); final XmppConnection xmppConnection = conversation.getAccount().getXmppConnection(); if (!message.hasFileOnRemoteHost() && xmppConnection != null diff --git a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java index be0949692d349b5437769a9ea116357f1b213174..b9731d669fc959c0d8604ac3ac4e53e4987c9f5c 100644 --- a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java @@ -47,13 +47,14 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivitySearchBinding; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.adapter.MessageAdapter; +import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; import eu.siacs.conversations.ui.util.Color; import eu.siacs.conversations.ui.util.Drawable; import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.showKeyboard; -public class SearchActivity extends XmppActivity implements TextWatcher { +public class SearchActivity extends XmppActivity implements TextWatcher, OnSearchResultsAvailable { private ActivitySearchBinding binding; private MessageAdapter messageListAdapter; @@ -122,7 +123,23 @@ public class SearchActivity extends XmppActivity implements TextWatcher { @Override public void afterTextChanged(Editable s) { - Log.d(Config.LOGTAG,"searching for "+s); + String term = s.toString().trim(); + if (term.length() > 0) { + xmppConnectionService.search(s.toString().trim(), this); + } else { + this.messages.clear(); + messageListAdapter.notifyDataSetChanged(); + changeBackground(false, false); + } } + @Override + public void onSearchResultsAvailable(String term, List messages) { + this.messages.clear(); + this.messages.addAll(messages); + runOnUiThread(() -> { + messageListAdapter.notifyDataSetChanged(); + changeBackground(true, messages.size() > 0); + }); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 3c267c77ab5fdf98a9a3b61472851a63281d7b05..abe490daea4bbda8a2b2e4e4881c9bc0d5a5aa61 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -87,11 +87,12 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer } else { resId = R.string.shared_file_with_x; } - replaceToast(getString(resId, message.getConversation().getName())); + Conversation conversation = (Conversation) message.getConversation(); + replaceToast(getString(resId, conversation.getName())); if (mReturnToPrevious) { finish(); } else { - switchToConversation(message.getConversation()); + switchToConversation(conversation); } } }); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 6c16c634b8cd60a6c3d63ef694b35016f004470b..65e05cf7dca99c7c0c3b9ad4012708b94bf6eb33 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -58,6 +58,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message.FileParams; @@ -112,15 +113,15 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie private static String removeTrailingBracket(final String url) { int numOpenBrackets = 0; - for(char c : url.toCharArray()) { - if (c=='(') { + for (char c : url.toCharArray()) { + if (c == '(') { ++numOpenBrackets; - } else if (c==')') { + } else if (c == ')') { --numOpenBrackets; } } if (numOpenBrackets != 0 && url.charAt(url.length() - 1) == ')') { - return url.substring(0,url.length() - 1); + return url.substring(0, url.length() - 1); } else { return url; } @@ -530,10 +531,13 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie } } if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) { - Pattern pattern = NotificationService.generateNickHighlightPattern(message.getConversation().getMucOptions().getActualNick()); - Matcher matcher = pattern.matcher(body); - while (matcher.find()) { - body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (message.getConversation() instanceof Conversation) { + final Conversation conversation = (Conversation) message.getConversation(); + Pattern pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualNick()); + Matcher matcher = pattern.matcher(body); + while (matcher.find()) { + body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } } } Matcher matcher = Emoticons.generatePattern(body).matcher(body); @@ -649,7 +653,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie final Message message = getItem(position); final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted()); - final Conversation conversation = message.getConversation(); + final Conversational conversation = message.getConversation(); final Account account = conversation.getAccount(); final int type = getItemViewType(position); ViewHolder viewHolder; @@ -727,7 +731,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie viewHolder.status_message.setVisibility(View.GONE); viewHolder.contact_picture.setVisibility(View.GONE); viewHolder.load_more_messages.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages(message.getConversation())); + viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages((Conversation) message.getConversation())); } else { viewHolder.status_message.setVisibility(View.VISIBLE); viewHolder.load_more_messages.setVisibility(View.GONE); @@ -794,7 +798,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie } } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { if (account.isPgpDecryptionServiceConnected()) { - if (!account.hasPendingPgpIntent(conversation)) { + if (conversation instanceof Conversation && !account.hasPendingPgpIntent((Conversation) conversation)) { displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground); } else { displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground); @@ -863,7 +867,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie } private static void resetClickListener(View... views) { - for(View view : views) { + for (View view : views) { view.setOnClickListener(null); } } diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnSearchResultsAvailable.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnSearchResultsAvailable.java new file mode 100644 index 0000000000000000000000000000000000000000..d884af3617dec47133388fa2893230697882624e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/interfaces/OnSearchResultsAvailable.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.siacs.conversations.ui.interfaces; + +import java.util.List; + +import eu.siacs.conversations.entities.Message; + +public interface OnSearchResultsAvailable { + + void onSearchResultsAvailable(String term, List messages); + +} diff --git a/src/main/java/eu/siacs/conversations/utils/Cancellable.java b/src/main/java/eu/siacs/conversations/utils/Cancellable.java new file mode 100644 index 0000000000000000000000000000000000000000..d844856c06b20145de9be63682c7250a2edfda4f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/Cancellable.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.siacs.conversations.utils; + +public interface Cancellable { + void cancel(); +} diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java index 4ba13694726d7dd473161ff7a8c8b720e397db87..0c15355479f937473dd8c8f90aa4c2f2feed32c2 100644 --- a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java @@ -11,6 +11,7 @@ import java.util.regex.Pattern; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; public class GeoHelper { @@ -38,11 +39,11 @@ public class GeoHelper { } catch (NumberFormatException nfe) { return intents; } - final Conversation conversation = message.getConversation(); + final Conversational conversation = message.getConversation(); String label; - if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) { + if (conversation instanceof Conversation && conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) { try { - label = "(" + URLEncoder.encode(message.getConversation().getName().toString(), "UTF-8") + ")"; + label = "(" + URLEncoder.encode(((Conversation)conversation).getName().toString(), "UTF-8") + ")"; } catch (UnsupportedEncodingException e) { label = ""; } diff --git a/src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java b/src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java index 321dd9c894868d2e9a7b9bee0adce62f274c3fda..601b995b8395033c4fe30e4c8b6d6d203387a81b 100644 --- a/src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java +++ b/src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java @@ -2,6 +2,10 @@ package eu.siacs.conversations.utils; public class ReplacingSerialSingleThreadExecutor extends SerialSingleThreadExecutor { + public ReplacingSerialSingleThreadExecutor(String name) { + super(name, false); + } + public ReplacingSerialSingleThreadExecutor(boolean prepareLooper) { super(ReplacingSerialSingleThreadExecutor.class.getName(), prepareLooper); } @@ -9,6 +13,9 @@ public class ReplacingSerialSingleThreadExecutor extends SerialSingleThreadExecu @Override public synchronized void execute(final Runnable r) { tasks.clear(); + if (active != null && active instanceof Cancellable) { + ((Cancellable) active).cancel(); + } super.execute(r); } } diff --git a/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java index c25ed10cc6e099732d70a57983e5b8a27587f09b..768fc7a677923feef1c9f4699c96e878d9f8f7e7 100644 --- a/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java +++ b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java @@ -15,7 +15,7 @@ public class SerialSingleThreadExecutor implements Executor { private final Executor executor = Executors.newSingleThreadExecutor(); final ArrayDeque tasks = new ArrayDeque<>(); - private Runnable active; + protected Runnable active; private final String name; public SerialSingleThreadExecutor(String name) { @@ -43,7 +43,7 @@ public class SerialSingleThreadExecutor implements Executor { } private synchronized void scheduleNext() { - if ((active = tasks.poll()) != null) { + if ((active = tasks.poll()) != null) { executor.execute(active); int remaining = tasks.size(); if (remaining > 0) { diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 2957f1ab4b3548cfd3101a5ce5f7de727f71f207..1054a52baec0f7a1a3d700135c8a2449d014abfa 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -21,6 +21,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; @@ -448,7 +449,7 @@ public class UIHelper { } public static String getMessageDisplayName(final Message message) { - final Conversation conversation = message.getConversation(); + final Conversational conversation = message.getConversation(); if (message.getStatus() == Message.STATUS_RECEIVED) { final Contact contact = message.getContact(); if (conversation.getMode() == Conversation.MODE_MULTI) { @@ -461,8 +462,8 @@ public class UIHelper { return contact != null ? contact.getDisplayName() : ""; } } else { - if (conversation.getMode() == Conversation.MODE_MULTI) { - return conversation.getMucOptions().getSelf().getName(); + if (conversation instanceof Conversation && conversation.getMode() == Conversation.MODE_MULTI) { + return ((Conversation) conversation).getMucOptions().getSelf().getName(); } else { final Jid jid = conversation.getAccount().getJid(); return jid.getLocal() != null ? jid.getLocal() : Jid.ofDomain(jid.getDomain()).toString(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 99ec5875ce7ad587a8379de800feb9bf8f432294..b50072a8cd8e21bd85bcb95a997b701bd801e641 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -261,7 +261,7 @@ public class JingleConnection implements Transferable { public void init(final Message message) { if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - Conversation conversation = message.getConversation(); + Conversation conversation = (Conversation) message.getConversation(); conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, new OnMessageCreatedCallback() { @Override public void run(XmppAxolotlMessage xmppAxolotlMessage) { diff --git a/src/main/res/layout/activity_search.xml b/src/main/res/layout/activity_search.xml index bce914b16fd6ca51ae5a43a9bb760aa9ea91e04a..150ab2d36ab446ec0e5094ccf5f121926b6c766e 100644 --- a/src/main/res/layout/activity_search.xml +++ b/src/main/res/layout/activity_search.xml @@ -46,6 +46,8 @@ android:layout_height="match_parent" android:background="?attr/activity_background_search" android:divider="@android:color/transparent" - android:dividerHeight="0dp"/> + android:dividerHeight="0dp" + android:listSelector="@android:color/transparent" + android:stackFromBottom="true"/> \ No newline at end of file