From 64ba6d1db7994a457ca3f88b12872b17c42dbae5 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 2 Mar 2022 15:10:44 -0500 Subject: [PATCH] Store oobUri and preserve message body Instead of assuming that body must contain the OOB URI if present, store it independently. Instead of overwriting the body with file metadata, store the metadata in its own location. --- .../entities/IndividualMessage.java | 2 +- .../siacs/conversations/entities/Message.java | 119 ++++++++++++------ .../http/HttpDownloadConnection.java | 8 +- .../conversations/parser/MessageParser.java | 6 +- .../persistance/DatabaseBackend.java | 18 ++- .../persistance/FileBackend.java | 20 +-- .../conversations/utils/MessageUtils.java | 2 + 7 files changed, 120 insertions(+), 55 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java index 0d71d5782667e03866c1f035a0cbcf892a2656a9..a9a62237aa21d6e0745d6c41eb157f79b7044998 100644 --- a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java +++ b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java @@ -44,7 +44,7 @@ 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, boolean deleted, String bodyLanguage) { - super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage, null); + super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, edited, oob, errorMessage, readByMarkers, markable, deleted, bodyLanguage, null, null, null); } @Override diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 87f43a3e4161d9753be5ae5e5c1464d3242cf517..a179465b8bb88c0026ded5ece8479ca87fa5274b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -13,6 +13,8 @@ import com.google.common.primitives.Longs; import org.json.JSONException; import java.lang.ref.WeakReference; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -101,7 +103,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable protected int type; protected boolean deleted = false; protected boolean carbon = false; - protected boolean oob = false; + private boolean oob = false; + protected URI oobUri = null; protected List edits = new ArrayList<>(); protected String relativeFilePath; protected boolean read = true; @@ -154,6 +157,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable false, false, null, + null, + null, null); } @@ -180,6 +185,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable false, false, null, + null, + null, null); } @@ -189,7 +196,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable 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, final boolean deleted, final String bodyLanguage, final String subject) { + final boolean markable, final boolean deleted, final String bodyLanguage, final String subject, final String oobUri, final String fileParams) { this.conversation = conversation; this.uuid = uuid; this.conversationUuid = conversationUUid; @@ -207,6 +214,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.axolotlFingerprint = fingerprint; this.read = read; this.edits = Edit.fromJson(edited); + setOob(oobUri); this.oob = oob; this.errorMessage = errorMessage; this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers; @@ -214,6 +222,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.deleted = deleted; this.bodyLanguage = bodyLanguage; this.subject = subject; + if (fileParams != null) this.fileParams = new FileParams(fileParams); } public static Message fromCursor(Cursor cursor, Conversation conversation) { @@ -240,7 +249,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)), - cursor.getString(cursor.getColumnIndex("subject")) + cursor.getString(cursor.getColumnIndex("subject")), + cursor.getString(cursor.getColumnIndex("oobUri")), + cursor.getString(cursor.getColumnIndex("fileParams")) ); } @@ -274,6 +285,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable ContentValues values = new ContentValues(); values.put(UUID, uuid); values.put("subject", subject); + values.put("oobUri", oobUri == null ? null : oobUri.toString()); + values.put("fileParams", fileParams == null ? null : fileParams.toString()); return values; } @@ -347,7 +360,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public String getBody() { - return body; + if (oobUri != null) { + return body.replace(oobUri.toString(), ""); + } else { + return body; + } } public synchronized void setBody(String body) { @@ -358,7 +375,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.isGeoUri = null; this.isEmojisOnly = null; this.treatAsDownloadable = null; - this.fileParams = null; } public String getSubject() { @@ -523,7 +539,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public synchronized void setTransferable(Transferable transferable) { - this.fileParams = null; this.transferable = transferable; } @@ -711,14 +726,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public boolean isOOb() { - return oob; + return oob || oobUri != null; } public static class MergeSeparator { } public SpannableStringBuilder getMergedBody() { - SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); + SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim()); Message current = this; while (current.mergeable(current.next())) { current = current.next(); @@ -806,8 +821,17 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } } - public void setOob(boolean isOob) { - this.oob = isOob; + public URI getOob() { + return oobUri; + } + + public void setOob(String oobUri) { + try { + this.oobUri = oobUri == null ? null : new URI(oobUri); + } catch (final URISyntaxException e) { + this.oobUri = null; + } + this.oob = this.oobUri != null; } public String getMimeType() { @@ -815,7 +839,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable if (relativeFilePath != null) { extension = MimeUtils.extractRelevantExtension(relativeFilePath); } else { - final String url = URL.tryParse(body.split("\n")[0]); + final String url = URL.tryParse(oobUri == null ? body.split("\n")[0] : oobUri.toString()); if (url == null) { return null; } @@ -826,7 +850,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public synchronized boolean treatAsDownloadable() { if (treatAsDownloadable == null) { - treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob); + treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, isOOb()); } return treatAsDownloadable; } @@ -849,35 +873,19 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.fileParams = null; } + public synchronized void setFileParams(FileParams fileParams) { + this.fileParams = fileParams; + } + public synchronized FileParams getFileParams() { if (fileParams == null) { - fileParams = new FileParams(); + fileParams = new FileParams(this.body); if (this.transferable != null) { fileParams.size = this.transferable.getFileSize(); } - final String[] parts = body == null ? new String[0] : body.split("\\|"); - switch (parts.length) { - case 1: - try { - fileParams.size = Long.parseLong(parts[0]); - } catch (final NumberFormatException e) { - fileParams.url = URL.tryParse(parts[0]); - } - break; - case 5: - fileParams.runtime = parseInt(parts[4]); - case 4: - fileParams.width = parseInt(parts[2]); - fileParams.height = parseInt(parts[3]); - case 2: - fileParams.url = URL.tryParse(parts[0]); - fileParams.size = Longs.tryParse(parts[1]); - break; - case 3: - fileParams.size = Longs.tryParse(parts[0]); - fileParams.width = parseInt(parts[1]); - fileParams.height = parseInt(parts[2]); - break; + + if (oobUri != null && ("http".equalsIgnoreCase(oobUri.getScheme()) || "https".equalsIgnoreCase(oobUri.getScheme()))) { + fileParams.url = oobUri.toString(); } } return fileParams; @@ -924,9 +932,48 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public int height = 0; public int runtime = 0; + public FileParams() { } + + public FileParams(String ser) { + final String[] parts = ser == null ? new String[0] : ser.split("\\|"); + switch (parts.length) { + case 1: + try { + this.size = Long.parseLong(parts[0]); + } catch (final NumberFormatException e) { + this.url = URL.tryParse(parts[0]); + } + break; + case 5: + this.runtime = parseInt(parts[4]); + case 4: + this.width = parseInt(parts[2]); + this.height = parseInt(parts[3]); + case 2: + this.url = URL.tryParse(parts[0]); + this.size = Longs.tryParse(parts[1]); + break; + case 3: + this.size = Longs.tryParse(parts[0]); + this.width = parseInt(parts[1]); + this.height = parseInt(parts[2]); + break; + } + } + public long getSize() { return size == null ? 0 : size; } + + public String toString() { + final StringBuilder builder = new StringBuilder(); + if (url != null) builder.append(url); + if (size != null) builder.append('|').append(size.toString()); + if (width > 0 || height > 0 || runtime > 0) builder.append('|').append(width); + if (height > 0 || runtime > 0) builder.append('|').append(height); + if (runtime > 0) builder.append('|').append(runtime); + return builder.toString(); + } } public void setFingerprint(String fingerprint) { diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 15dc6eac692a96dc51e5e618764accb3e0fb6b75..cca15866be6145ef95ad03f98750e336fc8e98f8 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -68,22 +68,22 @@ public class HttpDownloadConnection implements Transferable { } public void init(boolean interactive) { + final Message.FileParams fileParams = message.getFileParams(); if (message.isDeleted()) { if (message.getType() == Message.TYPE_PRIVATE_FILE) { message.setType(Message.TYPE_PRIVATE); } else if (message.isFileOrImage()) { message.setType(Message.TYPE_TEXT); } - message.setOob(true); + message.setOob(fileParams.url); message.setDeleted(false); mXmppConnectionService.updateMessage(message); } this.message.setTransferable(this); try { - final Message.FileParams fileParams = message.getFileParams(); if (message.hasFileOnRemoteHost()) { mUrl = AesGcmURL.of(fileParams.url); - } else if (message.isOOb() && fileParams.url != null && fileParams.size != null) { + } else if (message.isOOb() && fileParams.url != null) { mUrl = AesGcmURL.of(fileParams.url); } else { mUrl = AesGcmURL.of(message.getBody().split("\n")[0]); @@ -288,7 +288,7 @@ public class HttpDownloadConnection implements Transferable { } final Message.FileParams fileParams = message.getFileParams(); FileBackend.updateFileParams(message, fileParams.url, size); - message.setOob(true); + message.setOob(fileParams.url); mXmppConnectionService.databaseBackend.updateMessage(message, true); file.setExpectedSize(size); message.resetFileParams(); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index c96a2ff65c449929a6789146128049abd79bb15f..186562420b214670f0ddf2b6159c6b2efc884991 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -560,7 +560,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } else if (body == null && oobUrl != null) { message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status); - message.setOob(true); + message.setOob(oobUrl); if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) { message.setEncryption(Message.ENCRYPTION_DECRYPTED); } @@ -577,8 +577,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece message.setServerMsgId(serverMsgId); message.setCarbon(isCarbon); message.setTime(timestamp); - if (body != null && body.content != null && body.content.equals(oobUrl)) { - message.setOob(true); + if (oobUrl != null) { + message.setOob(oobUrl); if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) { message.setEncryption(Message.ENCRYPTION_DECRYPTED); } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 08bffb9da09296d1073ce27a4a5912e63ac2568c..a1c4caa4a5bab56dc409e26e56cb1f75dd2d2e91 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -232,6 +232,18 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("PRAGMA cheogram.user_version = 1"); } + if(cheogramVersion < 2) { + db.execSQL( + "ALTER TABLE cheogram." + Message.TABLENAME + " " + + "ADD COLUMN oobUri TEXT" + ); + db.execSQL( + "ALTER TABLE cheogram." + Message.TABLENAME + " " + + "ADD COLUMN fileParams TEXT" + ); + db.execSQL("PRAGMA cheogram.user_version = 2"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -1063,13 +1075,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (!includeBody) { contentValues.remove(Message.BODY); } - return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1; + return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1 && + db.update("cheogram." + Message.TABLENAME, message.getCheogramContentValues(), Message.UUID + "=?", args) == 1; } public boolean updateMessage(Message message, String uuid) { SQLiteDatabase db = this.getWritableDatabase(); String[] args = {uuid}; - return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1; + return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1 && + db.update("cheogram." + Message.TABLENAME, message.getCheogramContentValues(), Message.UUID + "=?", args) == 1; } public void readRoster(Roster roster) { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 2cef93b002735d492602c1af633de1384c4f3110..e1584b143b448ba8e4a852094709e5b8dcdcc7ce 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -418,9 +418,10 @@ public class FileBackend { } public static void updateFileParams(Message message, String url, long size) { - final StringBuilder body = new StringBuilder(); - body.append(url).append('|').append(size); - message.setBody(body.toString()); + Message.FileParams fileParams = new Message.FileParams(); + fileParams.url = url; + fileParams.size = size; + message.setFileParams(fileParams); } public Bitmap getPreviewForUri(Attachment attachment, int size, boolean cacheOnly) { @@ -1344,11 +1345,11 @@ public class FileBackend { final boolean video = mime != null && mime.startsWith("video/"); final boolean audio = mime != null && mime.startsWith("audio/"); final boolean pdf = "application/pdf".equals(mime); - final StringBuilder body = new StringBuilder(); + Message.FileParams fileParams = new Message.FileParams(); if (url != null) { - body.append(url); + fileParams.url = url; } - body.append('|').append(file.getSize()); + fileParams.size = file.getSize(); if (image || video || (pdf && Compatibility.runsTwentyOne())) { try { final Dimensions dimensions; @@ -1360,16 +1361,17 @@ public class FileBackend { dimensions = getImageDimensions(file); } if (dimensions.valid()) { - body.append('|').append(dimensions.width).append('|').append(dimensions.height); + fileParams.width = dimensions.width; + fileParams.height = dimensions.height; } } catch (NotAVideoFile notAVideoFile) { Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was not a video file"); //fall threw } } else if (audio) { - body.append("|0|0|").append(getMediaRuntime(file)); + fileParams.runtime = getMediaRuntime(file); } - message.setBody(body.toString()); + message.setFileParams(fileParams); message.setDeleted(false); message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE)); } diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index 0a11cd720412476334f94f8ae127b89a11f4ddfd..0f1636308bac74c422e746e46f852f5bf2ed8004 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -82,6 +82,8 @@ public class MessageUtils { } public static boolean treatAsDownloadable(final String body, final boolean oob) { + if (oob) return true; + final String[] lines = body.split("\n"); if (lines.length == 0) { return false;