diff --git a/build.gradle b/build.gradle index a4dd1ec17..f0aabc37c 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" + //implementation "com.squareup.okhttp3:logging-interceptor:3.14.9" implementation 'com.google.guava:guava:30.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18' implementation fileTree(include: ['libwebrtc-m89.aar'], dir: 'libs') diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index 29d286a71..2e992a687 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -209,7 +209,7 @@ public class PgpDecryptionService { message.setRelativeFilePath(path); } } - URL url = message.getFileParams().url; + final String url = message.getFileParams().url; mXmppConnectionService.getFileBackend().updateFileParams(message, url); message.setEncryption(Message.ENCRYPTION_DECRYPTED); mXmppConnectionService.updateMessage(message); diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 007a28f70..0d33bf79c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -75,7 +75,7 @@ public class PgpEngine { params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); String body; if (message.hasFileOnRemoteHost()) { - body = message.getFileParams().url.toString(); + body = message.getFileParams().url; } else { body = message.getBody(); } 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 67e8ca5da..6f233d871 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1169,7 +1169,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); final String content; if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url.toString(); + content = message.getFileParams().url; } else { content = message.getBody(); } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index ce39aa737..6a4b77179 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -147,7 +147,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public boolean httpUploadAvailable(long filesize) { - return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer()); + return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize); } public boolean httpUploadAvailable() { diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index a38dc5427..11314952c 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -788,7 +788,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { String otherBody; if (message.hasFileOnRemoteHost()) { - otherBody = message.getFileParams().url.toString(); + otherBody = message.getFileParams().url; } else { otherBody = message.body; } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 8c17efe12..258e9ba46 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -12,8 +12,6 @@ import com.google.common.collect.ImmutableSet; import org.json.JSONException; import java.lang.ref.WeakReference; -import java.net.MalformedURLException; -import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -22,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; +import eu.siacs.conversations.http.URL; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.utils.CryptoHelper; @@ -32,973 +31,966 @@ import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; -public class Message extends AbstractEntity implements AvatarService.Avatarable { - - public static final String TABLENAME = "messages"; - - public static final int STATUS_RECEIVED = 0; - public static final int STATUS_UNSEND = 1; - public static final int STATUS_SEND = 2; - public static final int STATUS_SEND_FAILED = 3; - public static final int STATUS_WAITING = 5; - public static final int STATUS_OFFERED = 6; - public static final int STATUS_SEND_RECEIVED = 7; - public static final int STATUS_SEND_DISPLAYED = 8; - - public static final int ENCRYPTION_NONE = 0; - public static final int ENCRYPTION_PGP = 1; - public static final int ENCRYPTION_OTR = 2; - public static final int ENCRYPTION_DECRYPTED = 3; - public static final int ENCRYPTION_DECRYPTION_FAILED = 4; - public static final int ENCRYPTION_AXOLOTL = 5; - public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6; - public static final int ENCRYPTION_AXOLOTL_FAILED = 7; - - public static final int TYPE_TEXT = 0; - public static final int TYPE_IMAGE = 1; - public static final int TYPE_FILE = 2; - public static final int TYPE_STATUS = 3; - public static final int TYPE_PRIVATE = 4; - public static final int TYPE_PRIVATE_FILE = 5; - public static final int TYPE_RTP_SESSION = 6; - - public static final String CONVERSATION = "conversationUuid"; - public static final String COUNTERPART = "counterpart"; - public static final String TRUE_COUNTERPART = "trueCounterpart"; - public static final String BODY = "body"; - public static final String BODY_LANGUAGE = "bodyLanguage"; - public static final String TIME_SENT = "timeSent"; - public static final String ENCRYPTION = "encryption"; - public static final String STATUS = "status"; - public static final String TYPE = "type"; - public static final String CARBON = "carbon"; - public static final String OOB = "oob"; - public static final String EDITED = "edited"; - public static final String REMOTE_MSG_ID = "remoteMsgId"; - public static final String SERVER_MSG_ID = "serverMsgId"; - public static final String RELATIVE_FILE_PATH = "relativeFilePath"; - public static final String FINGERPRINT = "axolotl_fingerprint"; - public static final String READ = "read"; - public static final String ERROR_MESSAGE = "errorMsg"; - public static final String READ_BY_MARKERS = "readByMarkers"; - public static final String MARKABLE = "markable"; - public static final String DELETED = "deleted"; - public static final String ME_COMMAND = "/me "; - - public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled"; - - - public boolean markable = false; - protected String conversationUuid; - protected Jid counterpart; - protected Jid trueCounterpart; - protected String body; - protected String encryptedBody; - protected long timeSent; - protected int encryption; - protected int status; - protected int type; - protected boolean deleted = false; - protected boolean carbon = false; - protected boolean oob = false; - protected List edits = new ArrayList<>(); - protected String relativeFilePath; - protected boolean read = true; - protected String remoteMsgId = null; - private String bodyLanguage = null; - protected String serverMsgId = null; - private final Conversational conversation; - protected Transferable transferable = null; - private Message mNextMessage = null; - private Message mPreviousMessage = null; - private String axolotlFingerprint = null; - private String errorMessage = null; - private Set readByMarkers = new CopyOnWriteArraySet<>(); - - private Boolean isGeoUri = null; - private Boolean isEmojisOnly = null; - private Boolean treatAsDownloadable = null; - private FileParams fileParams = null; - private List counterparts; - private WeakReference user; - - protected Message(Conversational conversation) { - this.conversation = conversation; - } - - public Message(Conversational conversation, String body, int encryption) { - this(conversation, body, encryption, STATUS_UNSEND); - } - - 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(), - null, - body, - System.currentTimeMillis(), - encryption, - status, - TYPE_TEXT, - false, - null, - null, - null, - null, - true, - null, - false, - null, - null, - false, - false, - null); - } - - public Message(Conversation conversation, int status, int type, final String remoteMsgId) { - this(conversation, java.util.UUID.randomUUID().toString(), - conversation.getUuid(), - conversation.getJid() == null ? null : conversation.getJid().asBareJid(), - null, - null, - System.currentTimeMillis(), - Message.ENCRYPTION_NONE, - status, - type, - false, - remoteMsgId, - null, - null, - null, - true, - null, - false, - null, - null, - false, - false, - null); - } - - 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, final boolean deleted, final String bodyLanguage) { - this.conversation = conversation; - this.uuid = uuid; - this.conversationUuid = conversationUUid; - this.counterpart = counterpart; - this.trueCounterpart = trueCounterpart; - this.body = body == null ? "" : body; - this.timeSent = timeSent; - this.encryption = encryption; - this.status = status; - this.type = type; - this.carbon = carbon; - this.remoteMsgId = remoteMsgId; - this.relativeFilePath = relativeFilePath; - this.serverMsgId = serverMsgId; - this.axolotlFingerprint = fingerprint; - this.read = read; - this.edits = Edit.fromJson(edited); - this.oob = oob; - this.errorMessage = errorMessage; - this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers; - this.markable = markable; - this.deleted = deleted; - this.bodyLanguage = bodyLanguage; - } - - public static Message fromCursor(Cursor cursor, Conversation conversation) { - return new Message(conversation, - cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(CONVERSATION)), - fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))), - fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))), - 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, - cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, - cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)) - ); - } - - private static Jid fromString(String value) { - try { - if (value != null) { - return Jid.of(value); - } - } catch (IllegalArgumentException e) { - return null; - } - return null; - } - - public static Message createStatusMessage(Conversation conversation, String body) { - final Message message = new Message(conversation); - message.setType(Message.TYPE_STATUS); - message.setStatus(Message.STATUS_RECEIVED); - message.body = body; - return message; - } - - public static Message createLoadMoreMessage(Conversation conversation) { - final Message message = new Message(conversation); - message.setType(Message.TYPE_STATUS); - message.body = "LOAD_MORE"; - return message; - } - - @Override - public ContentValues getContentValues() { - ContentValues values = new ContentValues(); - values.put(UUID, uuid); - values.put(CONVERSATION, conversationUuid); - if (counterpart == null) { - values.putNull(COUNTERPART); - } else { - values.put(COUNTERPART, counterpart.toString()); - } - if (trueCounterpart == null) { - values.putNull(TRUE_COUNTERPART); - } 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(TIME_SENT, timeSent); - values.put(ENCRYPTION, encryption); - values.put(STATUS, status); - values.put(TYPE, type); - values.put(CARBON, carbon ? 1 : 0); - values.put(REMOTE_MSG_ID, remoteMsgId); - values.put(RELATIVE_FILE_PATH, relativeFilePath); - values.put(SERVER_MSG_ID, serverMsgId); - values.put(FINGERPRINT, axolotlFingerprint); - values.put(READ, read ? 1 : 0); - try { - values.put(EDITED, Edit.toJson(edits)); - } catch (JSONException e) { - Log.e(Config.LOGTAG,"error persisting json for edits",e); - } - values.put(OOB, oob ? 1 : 0); - values.put(ERROR_MESSAGE, errorMessage); - values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString()); - values.put(MARKABLE, markable ? 1 : 0); - values.put(DELETED, deleted ? 1 : 0); - values.put(BODY_LANGUAGE, bodyLanguage); - return values; - } - - public String getConversationUuid() { - return conversationUuid; - } - - public Conversational getConversation() { - return this.conversation; - } - - public Jid getCounterpart() { - return counterpart; - } - - public void setCounterpart(final Jid counterpart) { - this.counterpart = counterpart; - } - - public Contact getContact() { - if (this.conversation.getMode() == Conversation.MODE_SINGLE) { - return this.conversation.getContact(); - } else { - if (this.trueCounterpart == null) { - return null; - } else { - return this.conversation.getAccount().getRoster() - .getContactFromContactList(this.trueCounterpart); - } - } - } - - public String getBody() { - return body; - } - - public synchronized void setBody(String body) { - if (body == null) { - throw new Error("You should not set the message body to null"); - } - this.body = body; - this.isGeoUri = null; - this.isEmojisOnly = null; - this.treatAsDownloadable = null; - this.fileParams = null; - } - - public void setMucUser(MucOptions.User user) { - this.user = new WeakReference<>(user); - } - - public boolean sameMucUser(Message otherMessage) { - final MucOptions.User thisUser = this.user == null ? null : this.user.get(); - final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get(); - return thisUser != null && thisUser == otherUser; - } - - public String getErrorMessage() { - return errorMessage; - } - - public boolean setErrorMessage(String message) { - boolean changed = (message != null && !message.equals(errorMessage)) - || (message == null && errorMessage != null); - this.errorMessage = message; - return changed; - } - - public long getTimeSent() { - return timeSent; - } - - public int getEncryption() { - return encryption; - } - - public void setEncryption(int encryption) { - this.encryption = encryption; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getRelativeFilePath() { - return this.relativeFilePath; - } - - public void setRelativeFilePath(String path) { - this.relativeFilePath = path; - } - - public String getRemoteMsgId() { - return this.remoteMsgId; - } - - public void setRemoteMsgId(String id) { - this.remoteMsgId = id; - } - - public String getServerMsgId() { - return this.serverMsgId; - } - - public void setServerMsgId(String id) { - this.serverMsgId = id; - } - - public boolean isRead() { - return this.read; - } - - public boolean isDeleted() { - return this.deleted; - } - - public void setDeleted(boolean deleted) { - this.deleted = deleted; - } - - public void markRead() { - this.read = true; - } - - public void markUnread() { - this.read = false; - } - - public void setTime(long time) { - this.timeSent = time; - } - - public String getEncryptedBody() { - return this.encryptedBody; - } - - public void setEncryptedBody(String body) { - this.encryptedBody = body; - } - - public int getType() { - return this.type; - } - - public void setType(int type) { - this.type = type; - } - - public boolean isCarbon() { - return carbon; - } - - public void setCarbon(boolean carbon) { - this.carbon = carbon; - } - - public void putEdited(String edited, String serverMsgId) { - final Edit edit = new Edit(edited, serverMsgId); - if (this.edits.size() < 128 && !this.edits.contains(edit)) { - this.edits.add(edit); - } - } - - boolean remoteMsgIdMatchInEdit(String id) { - for(Edit edit : this.edits) { - if (id.equals(edit.getEditedId())) { - return true; - } - } - return false; - } - - public String getBodyLanguage() { - return this.bodyLanguage; - } - - public void setBodyLanguage(String language) { - this.bodyLanguage = language; - } - - public boolean edited() { - return this.edits.size() > 0; - } - - public void setTrueCounterpart(Jid trueCounterpart) { - this.trueCounterpart = trueCounterpart; - } - - public Jid getTrueCounterpart() { - return this.trueCounterpart; - } - - public Transferable getTransferable() { - return this.transferable; - } - - public synchronized void setTransferable(Transferable transferable) { - this.fileParams = null; - this.transferable = transferable; - } - - public boolean addReadByMarker(ReadByMarker readByMarker) { - if (readByMarker.getRealJid() != null) { - if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) { - return false; - } - } else if (readByMarker.getFullJid() != null) { - if (readByMarker.getFullJid().equals(counterpart)) { - return false; - } - } - if (this.readByMarkers.add(readByMarker)) { - if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) { - Iterator iterator = this.readByMarkers.iterator(); - while (iterator.hasNext()) { - ReadByMarker marker = iterator.next(); - if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) { - iterator.remove(); - } - } - } - return true; - } else { - return false; - } - } - - public Set getReadByMarkers() { - return ImmutableSet.copyOf(this.readByMarkers); - } - - boolean similar(Message message) { - if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) { - return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId()); - } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) { - return true; - } else if (this.body == null || this.counterpart == null) { - return false; - } else { - String body, otherBody; - if (this.hasFileOnRemoteHost()) { - body = getFileParams().url.toString(); - otherBody = message.body == null ? null : message.body.trim(); - } else { - body = this.body; - otherBody = message.body; - } - final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart()); - if (message.getRemoteMsgId() != null) { - final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches(); - if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) { - return true; - } - return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) - && matchingCounterpart - && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); - } else { - return this.remoteMsgId == null - && matchingCounterpart - && body.equals(otherBody) - && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; - } - } - } - - public Message next() { - 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; - } - } else { - throw new AssertionError("Calling next should be disabled for stubs"); - } - } - - public Message prev() { - 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) { - if (next.isEditable()) { - return false; - } - next = next.next(); - } - return isEditable(); - } - - public boolean isEditable() { - return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION; - } - - public boolean mergeable(final Message message) { - return message != null && - (message.getType() == Message.TYPE_TEXT && - this.getTransferable() == null && - message.getTransferable() == null && - message.getEncryption() != Message.ENCRYPTION_PGP && - message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && - this.getType() == message.getType() && - //this.getStatus() == message.getStatus() && - isStatusMergeable(this.getStatus(), message.getStatus()) && - this.getEncryption() == message.getEncryption() && - this.getCounterpart() != null && - this.getCounterpart().equals(message.getCounterpart()) && - 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() && - !this.isGeoUri() && - !message.isOOb() && - !this.isOOb() && - !message.treatAsDownloadable() && - !this.treatAsDownloadable() && - !message.hasMeCommand() && - !this.hasMeCommand() && - !this.bodyIsOnlyEmojis() && - !message.bodyIsOnlyEmojis() && - ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && - UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) && - this.getReadByMarkers().equals(message.getReadByMarkers()) && - !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS) - ); - } - - private static boolean isStatusMergeable(int a, int b) { - return a == b || ( - (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING) - || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING) - ); - } - - public void setCounterparts(List counterparts) { - this.counterparts = counterparts; - } - - public List getCounterparts() { - return this.counterparts; - } - - @Override - public int getAvatarBackgroundColor() { - if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) { - return Color.TRANSPARENT; - } else { - return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this)); - } - } - - @Override - public String getAvatarName() { - return UIHelper.getMessageDisplayName(this); - } - - public boolean isOOb() { - return oob; - } - - public static class MergeSeparator { - } - - public SpannableStringBuilder getMergedBody() { - SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - body.append("\n\n"); - body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), - SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); - body.append(MessageUtils.filterLtrRtl(current.getBody()).trim()); - } - return body; - } - - public boolean hasMeCommand() { - return this.body.trim().startsWith(ME_COMMAND); - } - - public int getMergedStatus() { - int status = this.status; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - status = current.status; - } - return status; - } - - public long getMergedTimeSent() { - long time = this.timeSent; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - time = current.timeSent; - } - return time; - } - - public boolean wasMergedIntoPrevious() { - Message prev = this.prev(); - return prev != null && prev.mergeable(this); - } - - public boolean trusted() { - Contact contact = this.getContact(); - return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf())); - } - - public boolean fixCounterpart() { - final Presences presences = conversation.getContact().getPresences(); - if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { - return true; - } else if (presences.size() >= 1) { - counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]); - return true; - } else { - counterpart = null; - return false; - } - } - - public void setUuid(String uuid) { - this.uuid = uuid; - } - - public String getEditedId() { - if (edits.size() > 0) { - return edits.get(edits.size() - 1).getEditedId(); - } else { - throw new IllegalStateException("Attempting to store unedited message"); - } - } - - public String getEditedIdWireFormat() { - if (edits.size() > 0) { - return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId(); - } else { - throw new IllegalStateException("Attempting to store unedited message"); - } - } - - public void setOob(boolean isOob) { - this.oob = isOob; - } - - public String getMimeType() { - String extension; - if (relativeFilePath != null) { - extension = MimeUtils.extractRelevantExtension(relativeFilePath); - } else { - try { - final URL url = new URL(body.split("\n")[0]); - extension = MimeUtils.extractRelevantExtension(url); - } catch (MalformedURLException e) { - return null; - } - } - return MimeUtils.guessMimeTypeFromExtension(extension); - } - - public synchronized boolean treatAsDownloadable() { - if (treatAsDownloadable == null) { - treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob); - } - return treatAsDownloadable; - } - - public synchronized boolean bodyIsOnlyEmojis() { - if (isEmojisOnly == null) { - isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", "")); - } - return isEmojisOnly; - } - - public synchronized boolean isGeoUri() { - if (isGeoUri == null) { - isGeoUri = GeoHelper.GEO_URI.matcher(body).matches(); - } - return isGeoUri; - } - - public synchronized void resetFileParams() { - this.fileParams = null; - } - - public synchronized FileParams getFileParams() { - if (fileParams == null) { - fileParams = new FileParams(); - 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 (NumberFormatException e) { - fileParams.url = parseUrl(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 = parseUrl(parts[0]); - fileParams.size = parseLong(parts[1]); - break; - case 3: - fileParams.size = parseLong(parts[0]); - fileParams.width = parseInt(parts[1]); - fileParams.height = parseInt(parts[2]); - break; - } - } - return fileParams; - } - - private static long parseLong(String value) { - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - return 0; - } - } - - private static int parseInt(String value) { - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return 0; - } - } - - private static URL parseUrl(String value) { - try { - return new URL(value); - } catch (MalformedURLException e) { - return null; - } - } - - public void untie() { - this.mNextMessage = null; - this.mPreviousMessage = null; - } - - public boolean isPrivateMessage() { - return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE; - } - - public boolean isFileOrImage() { - return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE; - } - - public boolean hasFileOnRemoteHost() { - return isFileOrImage() && getFileParams().url != null; - } - - public boolean needsUploading() { - return isFileOrImage() && getFileParams().url == null; - } - - public static class FileParams { - public URL url; - public long size = 0; - public int width = 0; - public int height = 0; - public int runtime = 0; - } - - public void setFingerprint(String fingerprint) { - this.axolotlFingerprint = fingerprint; - } - - public String getFingerprint() { - return axolotlFingerprint; - } - - public boolean isTrusted() { - FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint); - 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) { - continue; - } - return iterator.getEncryption(); - } - return ENCRYPTION_NONE; - } - - private int getNextEncryption() { - 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 conversation.getNextEncryption(); - } else { - throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs"); - } - } - - public boolean isValidInSession() { - int pastEncryption = getCleanedEncryption(this.getPreviousEncryption()); - int futureEncryption = getCleanedEncryption(this.getNextEncryption()); - - boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE - || futureEncryption == ENCRYPTION_NONE - || pastEncryption != futureEncryption; - - return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption; - } - - private static int getCleanedEncryption(int encryption) { - if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { - return ENCRYPTION_PGP; - } - if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) { - return ENCRYPTION_AXOLOTL; - } - return encryption; - } - - public static boolean configurePrivateMessage(final Message message) { - return configurePrivateMessage(message, false); - } - - public static boolean configurePrivateFileMessage(final Message message) { - return configurePrivateMessage(message, true); - } - - private static boolean configurePrivateMessage(final Message message, final boolean isFile) { - final Conversation conversation; - if (message.conversation instanceof Conversation) { - conversation = (Conversation) message.conversation; - } else { - return false; - } - if (conversation.getMode() == Conversation.MODE_MULTI) { - final Jid nextCounterpart = conversation.getNextCounterpart(); - if (nextCounterpart != null) { - message.setCounterpart(nextCounterpart); - message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart)); - message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE); - return true; - } - } - return false; - } +public class Message extends AbstractEntity implements AvatarService.Avatarable { + + public static final String TABLENAME = "messages"; + + public static final int STATUS_RECEIVED = 0; + public static final int STATUS_UNSEND = 1; + public static final int STATUS_SEND = 2; + public static final int STATUS_SEND_FAILED = 3; + public static final int STATUS_WAITING = 5; + public static final int STATUS_OFFERED = 6; + public static final int STATUS_SEND_RECEIVED = 7; + public static final int STATUS_SEND_DISPLAYED = 8; + + public static final int ENCRYPTION_NONE = 0; + public static final int ENCRYPTION_PGP = 1; + public static final int ENCRYPTION_OTR = 2; + public static final int ENCRYPTION_DECRYPTED = 3; + public static final int ENCRYPTION_DECRYPTION_FAILED = 4; + public static final int ENCRYPTION_AXOLOTL = 5; + public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6; + public static final int ENCRYPTION_AXOLOTL_FAILED = 7; + + public static final int TYPE_TEXT = 0; + public static final int TYPE_IMAGE = 1; + public static final int TYPE_FILE = 2; + public static final int TYPE_STATUS = 3; + public static final int TYPE_PRIVATE = 4; + public static final int TYPE_PRIVATE_FILE = 5; + public static final int TYPE_RTP_SESSION = 6; + + public static final String CONVERSATION = "conversationUuid"; + public static final String COUNTERPART = "counterpart"; + public static final String TRUE_COUNTERPART = "trueCounterpart"; + public static final String BODY = "body"; + public static final String BODY_LANGUAGE = "bodyLanguage"; + public static final String TIME_SENT = "timeSent"; + public static final String ENCRYPTION = "encryption"; + public static final String STATUS = "status"; + public static final String TYPE = "type"; + public static final String CARBON = "carbon"; + public static final String OOB = "oob"; + public static final String EDITED = "edited"; + public static final String REMOTE_MSG_ID = "remoteMsgId"; + public static final String SERVER_MSG_ID = "serverMsgId"; + public static final String RELATIVE_FILE_PATH = "relativeFilePath"; + public static final String FINGERPRINT = "axolotl_fingerprint"; + public static final String READ = "read"; + public static final String ERROR_MESSAGE = "errorMsg"; + public static final String READ_BY_MARKERS = "readByMarkers"; + public static final String MARKABLE = "markable"; + public static final String DELETED = "deleted"; + public static final String ME_COMMAND = "/me "; + + public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled"; + + + public boolean markable = false; + protected String conversationUuid; + protected Jid counterpart; + protected Jid trueCounterpart; + protected String body; + protected String encryptedBody; + protected long timeSent; + protected int encryption; + protected int status; + protected int type; + protected boolean deleted = false; + protected boolean carbon = false; + protected boolean oob = false; + protected List edits = new ArrayList<>(); + protected String relativeFilePath; + protected boolean read = true; + protected String remoteMsgId = null; + private String bodyLanguage = null; + protected String serverMsgId = null; + private final Conversational conversation; + protected Transferable transferable = null; + private Message mNextMessage = null; + private Message mPreviousMessage = null; + private String axolotlFingerprint = null; + private String errorMessage = null; + private Set readByMarkers = new CopyOnWriteArraySet<>(); + + private Boolean isGeoUri = null; + private Boolean isEmojisOnly = null; + private Boolean treatAsDownloadable = null; + private FileParams fileParams = null; + private List counterparts; + private WeakReference user; + + protected Message(Conversational conversation) { + this.conversation = conversation; + } + + public Message(Conversational conversation, String body, int encryption) { + this(conversation, body, encryption, STATUS_UNSEND); + } + + 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(), + null, + body, + System.currentTimeMillis(), + encryption, + status, + TYPE_TEXT, + false, + null, + null, + null, + null, + true, + null, + false, + null, + null, + false, + false, + null); + } + + public Message(Conversation conversation, int status, int type, final String remoteMsgId) { + this(conversation, java.util.UUID.randomUUID().toString(), + conversation.getUuid(), + conversation.getJid() == null ? null : conversation.getJid().asBareJid(), + null, + null, + System.currentTimeMillis(), + Message.ENCRYPTION_NONE, + status, + type, + false, + remoteMsgId, + null, + null, + null, + true, + null, + false, + null, + null, + false, + false, + null); + } + + 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, final boolean deleted, final String bodyLanguage) { + this.conversation = conversation; + this.uuid = uuid; + this.conversationUuid = conversationUUid; + this.counterpart = counterpart; + this.trueCounterpart = trueCounterpart; + this.body = body == null ? "" : body; + this.timeSent = timeSent; + this.encryption = encryption; + this.status = status; + this.type = type; + this.carbon = carbon; + this.remoteMsgId = remoteMsgId; + this.relativeFilePath = relativeFilePath; + this.serverMsgId = serverMsgId; + this.axolotlFingerprint = fingerprint; + this.read = read; + this.edits = Edit.fromJson(edited); + this.oob = oob; + this.errorMessage = errorMessage; + this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers; + this.markable = markable; + this.deleted = deleted; + this.bodyLanguage = bodyLanguage; + } + + public static Message fromCursor(Cursor cursor, Conversation conversation) { + return new Message(conversation, + cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(CONVERSATION)), + fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))), + fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))), + 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, + cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, + cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)) + ); + } + + private static Jid fromString(String value) { + try { + if (value != null) { + return Jid.of(value); + } + } catch (IllegalArgumentException e) { + return null; + } + return null; + } + + public static Message createStatusMessage(Conversation conversation, String body) { + final Message message = new Message(conversation); + message.setType(Message.TYPE_STATUS); + message.setStatus(Message.STATUS_RECEIVED); + message.body = body; + return message; + } + + public static Message createLoadMoreMessage(Conversation conversation) { + final Message message = new Message(conversation); + message.setType(Message.TYPE_STATUS); + message.body = "LOAD_MORE"; + return message; + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(CONVERSATION, conversationUuid); + if (counterpart == null) { + values.putNull(COUNTERPART); + } else { + values.put(COUNTERPART, counterpart.toString()); + } + if (trueCounterpart == null) { + values.putNull(TRUE_COUNTERPART); + } 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(TIME_SENT, timeSent); + values.put(ENCRYPTION, encryption); + values.put(STATUS, status); + values.put(TYPE, type); + values.put(CARBON, carbon ? 1 : 0); + values.put(REMOTE_MSG_ID, remoteMsgId); + values.put(RELATIVE_FILE_PATH, relativeFilePath); + values.put(SERVER_MSG_ID, serverMsgId); + values.put(FINGERPRINT, axolotlFingerprint); + values.put(READ, read ? 1 : 0); + try { + values.put(EDITED, Edit.toJson(edits)); + } catch (JSONException e) { + Log.e(Config.LOGTAG, "error persisting json for edits", e); + } + values.put(OOB, oob ? 1 : 0); + values.put(ERROR_MESSAGE, errorMessage); + values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString()); + values.put(MARKABLE, markable ? 1 : 0); + values.put(DELETED, deleted ? 1 : 0); + values.put(BODY_LANGUAGE, bodyLanguage); + return values; + } + + public String getConversationUuid() { + return conversationUuid; + } + + public Conversational getConversation() { + return this.conversation; + } + + public Jid getCounterpart() { + return counterpart; + } + + public void setCounterpart(final Jid counterpart) { + this.counterpart = counterpart; + } + + public Contact getContact() { + if (this.conversation.getMode() == Conversation.MODE_SINGLE) { + return this.conversation.getContact(); + } else { + if (this.trueCounterpart == null) { + return null; + } else { + return this.conversation.getAccount().getRoster() + .getContactFromContactList(this.trueCounterpart); + } + } + } + + public String getBody() { + return body; + } + + public synchronized void setBody(String body) { + if (body == null) { + throw new Error("You should not set the message body to null"); + } + this.body = body; + this.isGeoUri = null; + this.isEmojisOnly = null; + this.treatAsDownloadable = null; + this.fileParams = null; + } + + public void setMucUser(MucOptions.User user) { + this.user = new WeakReference<>(user); + } + + public boolean sameMucUser(Message otherMessage) { + final MucOptions.User thisUser = this.user == null ? null : this.user.get(); + final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get(); + return thisUser != null && thisUser == otherUser; + } + + public String getErrorMessage() { + return errorMessage; + } + + public boolean setErrorMessage(String message) { + boolean changed = (message != null && !message.equals(errorMessage)) + || (message == null && errorMessage != null); + this.errorMessage = message; + return changed; + } + + public long getTimeSent() { + return timeSent; + } + + public int getEncryption() { + return encryption; + } + + public void setEncryption(int encryption) { + this.encryption = encryption; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getRelativeFilePath() { + return this.relativeFilePath; + } + + public void setRelativeFilePath(String path) { + this.relativeFilePath = path; + } + + public String getRemoteMsgId() { + return this.remoteMsgId; + } + + public void setRemoteMsgId(String id) { + this.remoteMsgId = id; + } + + public String getServerMsgId() { + return this.serverMsgId; + } + + public void setServerMsgId(String id) { + this.serverMsgId = id; + } + + public boolean isRead() { + return this.read; + } + + public boolean isDeleted() { + return this.deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + + public void markRead() { + this.read = true; + } + + public void markUnread() { + this.read = false; + } + + public void setTime(long time) { + this.timeSent = time; + } + + public String getEncryptedBody() { + return this.encryptedBody; + } + + public void setEncryptedBody(String body) { + this.encryptedBody = body; + } + + public int getType() { + return this.type; + } + + public void setType(int type) { + this.type = type; + } + + public boolean isCarbon() { + return carbon; + } + + public void setCarbon(boolean carbon) { + this.carbon = carbon; + } + + public void putEdited(String edited, String serverMsgId) { + final Edit edit = new Edit(edited, serverMsgId); + if (this.edits.size() < 128 && !this.edits.contains(edit)) { + this.edits.add(edit); + } + } + + boolean remoteMsgIdMatchInEdit(String id) { + for (Edit edit : this.edits) { + if (id.equals(edit.getEditedId())) { + return true; + } + } + return false; + } + + public String getBodyLanguage() { + return this.bodyLanguage; + } + + public void setBodyLanguage(String language) { + this.bodyLanguage = language; + } + + public boolean edited() { + return this.edits.size() > 0; + } + + public void setTrueCounterpart(Jid trueCounterpart) { + this.trueCounterpart = trueCounterpart; + } + + public Jid getTrueCounterpart() { + return this.trueCounterpart; + } + + public Transferable getTransferable() { + return this.transferable; + } + + public synchronized void setTransferable(Transferable transferable) { + this.fileParams = null; + this.transferable = transferable; + } + + public boolean addReadByMarker(ReadByMarker readByMarker) { + if (readByMarker.getRealJid() != null) { + if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) { + return false; + } + } else if (readByMarker.getFullJid() != null) { + if (readByMarker.getFullJid().equals(counterpart)) { + return false; + } + } + if (this.readByMarkers.add(readByMarker)) { + if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) { + Iterator iterator = this.readByMarkers.iterator(); + while (iterator.hasNext()) { + ReadByMarker marker = iterator.next(); + if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) { + iterator.remove(); + } + } + } + return true; + } else { + return false; + } + } + + public Set getReadByMarkers() { + return ImmutableSet.copyOf(this.readByMarkers); + } + + boolean similar(Message message) { + if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) { + return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId()); + } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) { + return true; + } else if (this.body == null || this.counterpart == null) { + return false; + } else { + String body, otherBody; + if (this.hasFileOnRemoteHost()) { + body = getFileParams().url; + otherBody = message.body == null ? null : message.body.trim(); + } else { + body = this.body; + otherBody = message.body; + } + final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart()); + if (message.getRemoteMsgId() != null) { + final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches(); + if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) { + return true; + } + return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) + && matchingCounterpart + && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); + } else { + return this.remoteMsgId == null + && matchingCounterpart + && body.equals(otherBody) + && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; + } + } + } + + public Message next() { + 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; + } + } else { + throw new AssertionError("Calling next should be disabled for stubs"); + } + } + + public Message prev() { + 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) { + if (next.isEditable()) { + return false; + } + next = next.next(); + } + return isEditable(); + } + + public boolean isEditable() { + return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION; + } + + public boolean mergeable(final Message message) { + return message != null && + (message.getType() == Message.TYPE_TEXT && + this.getTransferable() == null && + message.getTransferable() == null && + message.getEncryption() != Message.ENCRYPTION_PGP && + message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && + this.getType() == message.getType() && + //this.getStatus() == message.getStatus() && + isStatusMergeable(this.getStatus(), message.getStatus()) && + this.getEncryption() == message.getEncryption() && + this.getCounterpart() != null && + this.getCounterpart().equals(message.getCounterpart()) && + 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() && + !this.isGeoUri() && + !message.isOOb() && + !this.isOOb() && + !message.treatAsDownloadable() && + !this.treatAsDownloadable() && + !message.hasMeCommand() && + !this.hasMeCommand() && + !this.bodyIsOnlyEmojis() && + !message.bodyIsOnlyEmojis() && + ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && + UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) && + this.getReadByMarkers().equals(message.getReadByMarkers()) && + !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS) + ); + } + + private static boolean isStatusMergeable(int a, int b) { + return a == b || ( + (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) + || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING) + || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING) + ); + } + + public void setCounterparts(List counterparts) { + this.counterparts = counterparts; + } + + public List getCounterparts() { + return this.counterparts; + } + + @Override + public int getAvatarBackgroundColor() { + if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) { + return Color.TRANSPARENT; + } else { + return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this)); + } + } + + @Override + public String getAvatarName() { + return UIHelper.getMessageDisplayName(this); + } + + public boolean isOOb() { + return oob; + } + + public static class MergeSeparator { + } + + public SpannableStringBuilder getMergedBody() { + SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); + Message current = this; + while (current.mergeable(current.next())) { + current = current.next(); + if (current == null) { + break; + } + body.append("\n\n"); + body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), + SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); + body.append(MessageUtils.filterLtrRtl(current.getBody()).trim()); + } + return body; + } + + public boolean hasMeCommand() { + return this.body.trim().startsWith(ME_COMMAND); + } + + public int getMergedStatus() { + int status = this.status; + Message current = this; + while (current.mergeable(current.next())) { + current = current.next(); + if (current == null) { + break; + } + status = current.status; + } + return status; + } + + public long getMergedTimeSent() { + long time = this.timeSent; + Message current = this; + while (current.mergeable(current.next())) { + current = current.next(); + if (current == null) { + break; + } + time = current.timeSent; + } + return time; + } + + public boolean wasMergedIntoPrevious() { + Message prev = this.prev(); + return prev != null && prev.mergeable(this); + } + + public boolean trusted() { + Contact contact = this.getContact(); + return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf())); + } + + public boolean fixCounterpart() { + final Presences presences = conversation.getContact().getPresences(); + if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { + return true; + } else if (presences.size() >= 1) { + counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]); + return true; + } else { + counterpart = null; + return false; + } + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getEditedId() { + if (edits.size() > 0) { + return edits.get(edits.size() - 1).getEditedId(); + } else { + throw new IllegalStateException("Attempting to store unedited message"); + } + } + + public String getEditedIdWireFormat() { + if (edits.size() > 0) { + return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId(); + } else { + throw new IllegalStateException("Attempting to store unedited message"); + } + } + + public void setOob(boolean isOob) { + this.oob = isOob; + } + + public String getMimeType() { + String extension; + if (relativeFilePath != null) { + extension = MimeUtils.extractRelevantExtension(relativeFilePath); + } else { + final String url = URL.tryParse(body.split("\n")[0]); + if (url == null) { + return null; + } + extension = MimeUtils.extractRelevantExtension(url); + } + return MimeUtils.guessMimeTypeFromExtension(extension); + } + + public synchronized boolean treatAsDownloadable() { + if (treatAsDownloadable == null) { + treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob); + } + return treatAsDownloadable; + } + + public synchronized boolean bodyIsOnlyEmojis() { + if (isEmojisOnly == null) { + isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", "")); + } + return isEmojisOnly; + } + + public synchronized boolean isGeoUri() { + if (isGeoUri == null) { + isGeoUri = GeoHelper.GEO_URI.matcher(body).matches(); + } + return isGeoUri; + } + + public synchronized void resetFileParams() { + this.fileParams = null; + } + + public synchronized FileParams getFileParams() { + if (fileParams == null) { + fileParams = new FileParams(); + 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 = parseLong(parts[1]); + break; + case 3: + fileParams.size = parseLong(parts[0]); + fileParams.width = parseInt(parts[1]); + fileParams.height = parseInt(parts[2]); + break; + } + } + return fileParams; + } + + private static long parseLong(String value) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return 0; + } + } + + private static int parseInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return 0; + } + } + + public void untie() { + this.mNextMessage = null; + this.mPreviousMessage = null; + } + + public boolean isPrivateMessage() { + return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE; + } + + public boolean isFileOrImage() { + return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE; + } + + public boolean hasFileOnRemoteHost() { + return isFileOrImage() && getFileParams().url != null; + } + + public boolean needsUploading() { + final boolean needsUploading = isFileOrImage() && getFileParams().url == null; + Log.d(Config.LOGTAG, "needs uploading " + needsUploading + " url=" + getFileParams().url); + return needsUploading; + } + + public static class FileParams { + public String url; + public long size = 0; + public int width = 0; + public int height = 0; + public int runtime = 0; + } + + public void setFingerprint(String fingerprint) { + this.axolotlFingerprint = fingerprint; + } + + public String getFingerprint() { + return axolotlFingerprint; + } + + public boolean isTrusted() { + FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint); + 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) { + continue; + } + return iterator.getEncryption(); + } + return ENCRYPTION_NONE; + } + + private int getNextEncryption() { + 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 conversation.getNextEncryption(); + } else { + throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs"); + } + } + + public boolean isValidInSession() { + int pastEncryption = getCleanedEncryption(this.getPreviousEncryption()); + int futureEncryption = getCleanedEncryption(this.getNextEncryption()); + + boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE + || futureEncryption == ENCRYPTION_NONE + || pastEncryption != futureEncryption; + + return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption; + } + + private static int getCleanedEncryption(int encryption) { + if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { + return ENCRYPTION_PGP; + } + if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) { + return ENCRYPTION_AXOLOTL; + } + return encryption; + } + + public static boolean configurePrivateMessage(final Message message) { + return configurePrivateMessage(message, false); + } + + public static boolean configurePrivateFileMessage(final Message message) { + return configurePrivateMessage(message, true); + } + + private static boolean configurePrivateMessage(final Message message, final boolean isFile) { + final Conversation conversation; + if (message.conversation instanceof Conversation) { + conversation = (Conversation) message.conversation; + } else { + return false; + } + if (conversation.getMode() == Conversation.MODE_MULTI) { + final Jid nextCounterpart = conversation.getNextCounterpart(); + if (nextCounterpart != null) { + message.setCounterpart(nextCounterpart); + message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart)); + message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE); + return true; + } + } + return false; + } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index ae9ce3bd5..198a2e71a 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -408,20 +408,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket requestP1S3Slot(Jid host, String md5) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(host); - packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5", md5); - return packet; - } - - public IqPacket requestP1S3Url(Jid host, String fileId) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(host); - packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("fileid", fileId); - return packet; - } - private static String convertFilename(String name) { int pos = name.indexOf('.'); if (pos != -1) { diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 3f1c5aa49..dd0291a56 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -14,7 +14,6 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -103,18 +102,9 @@ public class MessageGenerator extends AbstractGenerator { MessagePacket packet = preparePacket(message); String content; if (message.hasFileOnRemoteHost()) { - Message.FileParams fileParams = message.getFileParams(); - final URL url = fileParams.url; - if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { - Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER); - final String file = url.getFile(); - x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file); - x.setAttribute("fileid", url.getHost()); - return packet; - } else { - content = url.toString(); - packet.addChild("x", Namespace.OOB).addChild("url").setContent(content); - } + final Message.FileParams fileParams = message.getFileParams(); + content = fileParams.url; + packet.addChild("x", Namespace.OOB).addChild("url").setContent(content); } else { content = message.getBody(); } @@ -126,16 +116,9 @@ public class MessageGenerator extends AbstractGenerator { MessagePacket packet = preparePacket(message); if (message.hasFileOnRemoteHost()) { Message.FileParams fileParams = message.getFileParams(); - final URL url = fileParams.url; - if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { - Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER); - final String file = url.getFile(); - x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file); - x.setAttribute("fileid", url.getHost()); - } else { - packet.setBody(url.toString()); - packet.addChild("x", Namespace.OOB).addChild("url").setContent(url.toString()); - } + final String url = fileParams.url; + packet.setBody(url); + packet.addChild("x", Namespace.OOB).addChild("url").setContent(url); } else { if (Config.supportUnencrypted()) { packet.setBody(PGP_FALLBACK_MESSAGE); @@ -225,7 +208,7 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket received(Account account, final Jid from, final String id, ArrayList namespaces, int type) { + public MessagePacket received(Account account, final Jid from, final String id, ArrayList namespaces, int type) { final MessagePacket receivedPacket = new MessagePacket(); receivedPacket.setType(type); receivedPacket.setTo(from); diff --git a/src/main/java/eu/siacs/conversations/http/AesGcmURL.java b/src/main/java/eu/siacs/conversations/http/AesGcmURL.java new file mode 100644 index 000000000..6cacf64d7 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/AesGcmURL.java @@ -0,0 +1,41 @@ +package eu.siacs.conversations.http; + +import java.util.regex.Pattern; + +import okhttp3.HttpUrl; + +public final class AesGcmURL { + + /** + * This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors + */ + public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}"); + + public static final String PROTOCOL_NAME = "aesgcm"; + + private AesGcmURL() { + + } + + public static String toAesGcmUrl(HttpUrl url) { + if (url.isHttps()) { + return PROTOCOL_NAME + url.toString().substring(5); + } else { + return url.toString(); + } + } + + public static HttpUrl of(final String url) { + final int end = url.indexOf("://"); + if (end < 0) { + throw new IllegalArgumentException("Scheme not found"); + } + final String protocol = url.substring(0, end); + if (PROTOCOL_NAME.equals(protocol)) { + return HttpUrl.get("https" + url.substring(PROTOCOL_NAME.length())); + } else { + return HttpUrl.get(url); + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/http/AesGcmURLStreamHandler.java b/src/main/java/eu/siacs/conversations/http/AesGcmURLStreamHandler.java deleted file mode 100644 index 00b5985c2..000000000 --- a/src/main/java/eu/siacs/conversations/http/AesGcmURLStreamHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package eu.siacs.conversations.http; - -import java.io.IOException; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; -import java.util.regex.Pattern; - - -public class AesGcmURLStreamHandler extends URLStreamHandler { - - /** - * This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors - */ - public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}"); - - public static final String PROTOCOL_NAME = "aesgcm"; - - @Override - protected URLConnection openConnection(URL url) throws IOException { - return new URL("https"+url.toString().substring(url.getProtocol().length())).openConnection(); - } -} diff --git a/src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java b/src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java deleted file mode 100644 index 6974624e2..000000000 --- a/src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package eu.siacs.conversations.http; - -import java.net.URLStreamHandler; -import java.net.URLStreamHandlerFactory; - -public class CustomURLStreamHandlerFactory implements URLStreamHandlerFactory { - - @Override - public URLStreamHandler createURLStreamHandler(String protocol) { - if (AesGcmURLStreamHandler.PROTOCOL_NAME.equals(protocol)) { - return new AesGcmURLStreamHandler(); - } else if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(protocol)) { - return new P1S3UrlStreamHandler(); - } else { - return null; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 6a4e05f58..64b49f369 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -4,20 +4,19 @@ import android.util.Log; import org.apache.http.conn.ssl.StrictHostnameVerifier; -import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; -import java.net.URL; +import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; @@ -27,6 +26,8 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.TLSSocketFactory; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; public class HttpConnectionManager extends AbstractConnectionManager { @@ -39,8 +40,12 @@ public class HttpConnectionManager extends AbstractConnectionManager { super(service); } - public static Proxy getProxy() throws IOException { - return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050)); + public static Proxy getProxy() { + try { + return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050)); + } catch (final UnknownHostException e) { + throw new IllegalStateException(e); + } } public void createNewDownloadConnection(Message message) { @@ -75,15 +80,6 @@ public class HttpConnectionManager extends AbstractConnectionManager { } } - public boolean checkConnection(Message message) { - final Account account = message.getConversation().getAccount(); - final URL url = message.getFileParams().url; - if (url.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) && account.getStatus() != Account.State.ONLINE) { - return false; - } - return mXmppConnectionService.hasInternetConnection(); - } - void finishConnection(HttpDownloadConnection connection) { synchronized (this.downloadConnections) { this.downloadConnections.remove(connection); @@ -96,7 +92,21 @@ public class HttpConnectionManager extends AbstractConnectionManager { } } - void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) { + OkHttpClient buildHttpClient(final HttpUrl url, final Account account, boolean interactive) { + final String slotHostname = url.host(); + final boolean onionSlot = slotHostname.endsWith(".onion"); + final OkHttpClient.Builder builder = new OkHttpClient.Builder(); + //builder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS)); + builder.writeTimeout(30, TimeUnit.SECONDS); + builder.readTimeout(30, TimeUnit.SECONDS); + setupTrustManager(builder, interactive); + if (mXmppConnectionService.useTorToConnect() || account.isOnion() || onionSlot) { + builder.proxy(HttpConnectionManager.getProxy()).build(); + } + return builder.build(); + } + + private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) { final X509TrustManager trustManager; final HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive); if (interactive) { @@ -106,8 +116,8 @@ public class HttpConnectionManager extends AbstractConnectionManager { } try { final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG()); - connection.setSSLSocketFactory(sf); - connection.setHostnameVerifier(hostnameVerifier); + builder.sslSocketFactory(sf, trustManager); + builder.hostnameVerifier(hostnameVerifier); } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { } } diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index e55de385a..240d5dba7 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -1,29 +1,24 @@ package eu.siacs.conversations.http; -import android.os.PowerManager; import android.util.Log; import androidx.annotation.Nullable; import com.google.common.base.Strings; import com.google.common.io.ByteStreams; +import com.google.common.primitives.Longs; -import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; +import java.util.Locale; import java.util.concurrent.CancellationException; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLHandshakeException; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; @@ -33,8 +28,11 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.FileWriterException; import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.utils.WakeLockHelper; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import okhttp3.Call; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR; @@ -44,13 +42,13 @@ public class HttpDownloadConnection implements Transferable { private final boolean mUseTor; private final HttpConnectionManager mHttpConnectionManager; private final XmppConnectionService mXmppConnectionService; - private URL mUrl; + private HttpUrl mUrl; private DownloadableFile file; private int mStatus = Transferable.STATUS_UNKNOWN; private boolean acceptedAutomatically = false; private int mProgress = 0; private boolean canceled = false; - private Method method = Method.HTTP_UPLOAD; + private Call mostRecentCall; HttpDownloadConnection(Message message, HttpConnectionManager manager) { this.message = message; @@ -88,13 +86,13 @@ public class HttpDownloadConnection implements Transferable { try { final Message.FileParams fileParams = message.getFileParams(); if (message.hasFileOnRemoteHost()) { - mUrl = CryptoHelper.toHttpsUrl(fileParams.url); + mUrl = AesGcmURL.of(fileParams.url); } else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) { - mUrl = fileParams.url; + mUrl = AesGcmURL.of(fileParams.url); } else { - mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0])); + mUrl = AesGcmURL.of(message.getBody().split("\n")[0]); } - final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath()); + final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath()); if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { this.message.setEncryption(Message.ENCRYPTION_PGP); } else if (message.getEncryption() != Message.ENCRYPTION_OTR @@ -111,22 +109,22 @@ public class HttpDownloadConnection implements Transferable { if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); } - method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD; - long knownFileSize = message.getFileParams().size; - if (knownFileSize > 0 && interactive && method != Method.P1_S3) { + //TODO add auth tag size to knownFileSize + final long knownFileSize = message.getFileParams().size; + if (knownFileSize > 0 && interactive) { this.file.setExpectedSize(knownFileSize); download(true); } else { checkFileSize(interactive); } - } catch (MalformedURLException e) { + } catch (final IllegalArgumentException e) { this.cancel(); } } private void setupFile() { - final String reference = mUrl.getRef(); - if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) { + final String reference = mUrl.fragment(); + if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) { this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid()); this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")"); @@ -146,6 +144,10 @@ public class HttpDownloadConnection implements Transferable { @Override public void cancel() { this.canceled = true; + final Call call = this.mostRecentCall; + if (call != null && !call.isCanceled()) { + call.cancel(); + } mHttpConnectionManager.finishConnection(this); message.setTransferable(null); if (message.isFileOrImage()) { @@ -260,33 +262,7 @@ public class HttpDownloadConnection implements Transferable { @Override public void run() { - if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) { - retrieveUrl(); - } else { - check(); - } - } - - private void retrieveUrl() { - changeStatus(STATUS_CHECKING); - final Account account = message.getConversation().getAccount(); - IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(account.getDomain(), mUrl.getHost()); - mXmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - String download = packet.query().getAttribute("download"); - if (download != null) { - try { - mUrl = new URL(download); - check(); - return; - } catch (MalformedURLException e) { - //fallthrough - } - } - } - Log.d(Config.LOGTAG, "unable to retrieve actual download url"); - retrieveFailed(null); - }); + check(); } private void retrieveFailed(@Nullable Exception e) { @@ -330,46 +306,21 @@ public class HttpDownloadConnection implements Transferable { } private long retrieveFileSize() throws IOException { + final OkHttpClient client = mHttpConnectionManager.buildHttpClient( + mUrl, + message.getConversation().getAccount(), + interactive + ); + final Request request = new Request.Builder() + .url(URL.stripFragment(mUrl)) + .head() + .build(); + mostRecentCall = client.newCall(request); try { - Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive); - changeStatus(STATUS_CHECKING); - HttpURLConnection connection; - final String hostname = mUrl.getHost(); - final boolean onion = hostname != null && hostname.endsWith(".onion"); - if (mUseTor || message.getConversation().getAccount().isOnion() || onion) { - connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy()); - } else { - connection = (HttpURLConnection) mUrl.openConnection(); - } - if (method == Method.P1_S3) { - connection.setRequestMethod("GET"); - connection.addRequestProperty("Range", "bytes=0-0"); - } else { - connection.setRequestMethod("HEAD"); - } - connection.setUseCaches(false); - Log.d(Config.LOGTAG, "url: " + connection.getURL().toString()); - connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent()); - if (connection instanceof HttpsURLConnection) { - mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); - } - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.connect(); - String contentLength; - if (method == Method.P1_S3) { - String contentRange = connection.getHeaderField("Content-Range"); - String[] contentRangeParts = contentRange == null ? new String[0] : contentRange.split("/"); - if (contentRangeParts.length != 2) { - contentLength = null; - } else { - contentLength = contentRangeParts[1]; - } - } else { - contentLength = connection.getHeaderField("Content-Length"); - } - final String contentType = connection.getContentType(); - final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath()); + final Response response = mostRecentCall.execute(); + final String contentLength = response.header("Content-Length"); + final String contentType = response.header("Content-Type"); + final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath()); if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) { final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType); if (fileExtension != null) { @@ -378,8 +329,7 @@ public class HttpDownloadConnection implements Transferable { setupFile(); } } - connection.disconnect(); - if (contentLength == null) { + if (Strings.isNullOrEmpty(contentLength)) { throw new IOException("no content-length found in HEAD response"); } return Long.parseLong(contentLength, 10); @@ -397,8 +347,6 @@ public class HttpDownloadConnection implements Transferable { private final boolean interactive; - private OutputStream os; - public FileDownloader(boolean interactive) { this.interactive = interactive; } @@ -411,9 +359,10 @@ public class HttpDownloadConnection implements Transferable { decryptIfNeeded(); updateImageBounds(); finish(); - } catch (SSLHandshakeException e) { + } catch (final SSLHandshakeException e) { changeStatus(STATUS_OFFER); - } catch (Exception e) { + } catch (final Exception e) { + Log.d(Config.LOGTAG,"problem downloading",e); if (interactive) { showToastForException(e); } else { @@ -425,67 +374,57 @@ public class HttpDownloadConnection implements Transferable { } private void download() throws Exception { - InputStream is = null; - HttpURLConnection connection = null; - final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread()); - try { - wakeLock.acquire(); - if (mUseTor || message.getConversation().getAccount().isOnion()) { - connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy()); - } else { - connection = (HttpURLConnection) mUrl.openConnection(); - } - if (connection instanceof HttpsURLConnection) { - mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); - } - connection.setUseCaches(false); - connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent()); - final long expected = file.getExpectedSize(); - final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected; - long resumeSize = 0; + final OkHttpClient client = mHttpConnectionManager.buildHttpClient( + mUrl, + message.getConversation().getAccount(), + interactive + ); - if (tryResume) { - resumeSize = file.getSize(); - Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected); - connection.setRequestProperty("Range", "bytes=" + resumeSize + "-"); - } - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.connect(); - is = new BufferedInputStream(connection.getInputStream()); - final String contentRange = connection.getHeaderField("Content-Range"); - boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-"); + final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl)); + + final long expected = file.getExpectedSize(); + final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected; + final long resumeSize; + if (tryResume) { + resumeSize = file.getSize(); + Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected); + requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize)); + } else { + resumeSize = 0; + } + final Request request = requestBuilder.build(); + mostRecentCall = client.newCall(request); + final Response response = mostRecentCall.execute(); + final int code = response.code(); + if (code >= 200 && code <= 299) { + final String contentRange = response.header("Content-Range"); + final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-"); + final InputStream inputStream = response.body().byteStream(); + final OutputStream outputStream; long transmitted = 0; if (tryResume && serverResumed) { Log.d(Config.LOGTAG, "server resumed"); transmitted = file.getSize(); updateProgress(Math.round(((double) transmitted / expected) * 100)); - os = AbstractConnectionManager.createOutputStream(file, true, false); - if (os == null) { - throw new FileWriterException(); - } + outputStream = AbstractConnectionManager.createOutputStream(file, true, false); } else { - long reportedContentLengthOnGet; - try { - reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length")); - } catch (NumberFormatException | NullPointerException e) { - reportedContentLengthOnGet = 0; - } - if (expected != reportedContentLengthOnGet) { - Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")"); + final String contentLength = response.header("Content-Length"); + final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength); + if (expected != size) { + Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")"); } file.getParentFile().mkdirs(); if (!file.exists() && !file.createNewFile()) { throw new FileWriterException(); } - os = AbstractConnectionManager.createOutputStream(file, false, false); + outputStream = AbstractConnectionManager.createOutputStream(file, false, false); } int count; byte[] buffer = new byte[4096]; - while ((count = is.read(buffer)) != -1) { + while ((count = inputStream.read(buffer)) != -1) { transmitted += count; try { - os.write(buffer, 0, count); + outputStream.write(buffer, 0, count); } catch (IOException e) { throw new FileWriterException(); } @@ -494,35 +433,21 @@ public class HttpDownloadConnection implements Transferable { throw new CancellationException(); } } - try { - os.flush(); - } catch (IOException e) { - throw new FileWriterException(); - } - } catch (CancellationException | IOException e) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": http download failed", e); - throw e; - } finally { - FileBackend.close(os); - FileBackend.close(is); - if (connection != null) { - connection.disconnect(); - } - WakeLockHelper.release(wakeLock); + outputStream.flush(); + } else { + throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code)); } } private void updateImageBounds() { final boolean privateMessage = message.isPrivateMessage(); message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE); - final URL url; - final String ref = mUrl.getRef(); - if (method == Method.P1_S3) { - url = message.getFileParams().url; - } else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) { - url = CryptoHelper.toAesGcmUrl(mUrl); + final String url; + final String ref = mUrl.fragment(); + if (ref != null && AesGcmURL.IV_KEY.matcher(ref).matches()) { + url = AesGcmURL.toAesGcmUrl(mUrl); } else { - url = mUrl; + url = mUrl.toString(); } mXmppConnectionService.getFileBackend().updateFileParams(message, url); mXmppConnectionService.updateMessage(message); diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index a6161701f..92670a83d 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -1,250 +1,194 @@ package eu.siacs.conversations.http; -import android.os.PowerManager; import android.util.Log; -import java.io.FileInputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Scanner; +import org.jetbrains.annotations.NotNull; -import javax.net.ssl.HttpsURLConnection; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; -import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.Checksum; import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.WakeLockHelper; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; -import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR; +public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener { -public class HttpUploadConnection implements Transferable { + static final List WHITE_LISTED_HEADERS = Arrays.asList( + "Authorization", + "Cookie", + "Expires" + ); - static final List WHITE_LISTED_HEADERS = Arrays.asList( - "Authorization", - "Cookie", - "Expires" - ); + private final HttpConnectionManager mHttpConnectionManager; + private final XmppConnectionService mXmppConnectionService; + private final SlotRequester mSlotRequester; + private final Method method; + private final boolean mUseTor; + private boolean delayed = false; + private DownloadableFile file; + private final Message message; + private String mime; + private SlotRequester.Slot slot; + private byte[] key = null; - private final HttpConnectionManager mHttpConnectionManager; - private final XmppConnectionService mXmppConnectionService; - private final SlotRequester mSlotRequester; - private final Method method; - private final boolean mUseTor; - private boolean cancelled = false; - private boolean delayed = false; - private DownloadableFile file; - private final Message message; - private String mime; - private SlotRequester.Slot slot; - private byte[] key = null; + private long transmitted = 0; + private Call mostRecentCall; - private long transmitted = 0; + public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) { + this.message = message; + this.method = method; + this.mHttpConnectionManager = httpConnectionManager; + this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService(); + this.mSlotRequester = new SlotRequester(this.mXmppConnectionService); + this.mUseTor = mXmppConnectionService.useTorToConnect(); + } - public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) { - this.message = message; - this.method = method; - this.mHttpConnectionManager = httpConnectionManager; - this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService(); - this.mSlotRequester = new SlotRequester(this.mXmppConnectionService); - this.mUseTor = mXmppConnectionService.useTorToConnect(); - } + @Override + public boolean start() { + return false; + } - @Override - public boolean start() { - return false; - } + @Override + public int getStatus() { + return STATUS_UPLOADING; + } - @Override - public int getStatus() { - return STATUS_UPLOADING; - } + @Override + public long getFileSize() { + return file == null ? 0 : file.getExpectedSize(); + } - @Override - public long getFileSize() { - return file == null ? 0 : file.getExpectedSize(); - } + @Override + public int getProgress() { + if (file == null) { + return 0; + } + return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); + } - @Override - public int getProgress() { - if (file == null) { - return 0; - } - return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); - } + @Override + public void cancel() { + final Call call = this.mostRecentCall; + if (call != null && !call.isCanceled()) { + call.cancel(); + } + } - @Override - public void cancel() { - this.cancelled = true; - } + private void fail(String errorMessage) { + finish(); + final Call call = this.mostRecentCall; + final boolean cancelled = call != null && call.isCanceled(); + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage); + } - private void fail(String errorMessage) { - finish(); - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage); - } + private void finish() { + mHttpConnectionManager.finishUploadConnection(this); + message.setTransferable(null); + } - private void finish() { - mHttpConnectionManager.finishUploadConnection(this); - message.setTransferable(null); - } + public void init(boolean delay) { + final Account account = message.getConversation().getAccount(); + this.file = mXmppConnectionService.getFileBackend().getFile(message, false); + if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + this.mime = "application/pgp-encrypted"; + } else { + this.mime = this.file.getMimeType(); + } + final long originalFileSize = file.getSize(); + this.delayed = delay; + if (Config.ENCRYPT_ON_HTTP_UPLOADED + || message.getEncryption() == Message.ENCRYPTION_AXOLOTL + || message.getEncryption() == Message.ENCRYPTION_OTR) { + this.key = new byte[44]; + mXmppConnectionService.getRNG().nextBytes(this.key); + this.file.setKeyAndIv(this.key); + } + this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); + message.resetFileParams(); + this.mSlotRequester.request(method, account, file, mime, new SlotRequester.OnSlotRequested() { + @Override + public void success(final SlotRequester.Slot slot) { + //TODO needs to mark the message as cancelled afterwards (ie call fail()) + HttpUploadConnection.this.slot = slot; + HttpUploadConnection.this.upload(); + } - public void init(boolean delay) { - final Account account = message.getConversation().getAccount(); - this.file = mXmppConnectionService.getFileBackend().getFile(message, false); - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - this.mime = "application/pgp-encrypted"; - } else { - this.mime = this.file.getMimeType(); - } - final long originalFileSize = file.getSize(); - this.delayed = delay; - if (Config.ENCRYPT_ON_HTTP_UPLOADED - || message.getEncryption() == Message.ENCRYPTION_AXOLOTL - || message.getEncryption() == Message.ENCRYPTION_OTR) { - this.key = new byte[44]; - mXmppConnectionService.getRNG().nextBytes(this.key); - this.file.setKeyAndIv(this.key); - } + @Override + public void failure(String message) { + fail(message); + } + }); + message.setTransferable(this); + mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); + } - final String md5; + private void upload() { + final OkHttpClient client = mHttpConnectionManager.buildHttpClient( + slot.put, + message.getConversation().getAccount(), + true + ); + final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this); + final Request request = new Request.Builder() + .url(slot.put) + .put(requestBody) + .headers(slot.headers) + .build(); + Log.d(Config.LOGTAG, "uploading file to " + slot.put); + this.mostRecentCall = client.newCall(request); + this.mostRecentCall.enqueue(new Callback() { + @Override + public void onFailure(@NotNull Call call, IOException e) { + Log.d(Config.LOGTAG, "http upload failed", e); + fail(e.getMessage()); + } - if (method == Method.P1_S3) { - try { - md5 = Checksum.md5(AbstractConnectionManager.upgrade(file, new FileInputStream(file))); - } catch (Exception e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to calculate md5()", e); - fail(e.getMessage()); - return; - } - } else { - md5 = null; - } + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + final int code = response.code(); + if (code == 200 || code == 201) { + Log.d(Config.LOGTAG, "finished uploading file"); + final String get; + if (key != null) { + get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build()); + } else { + get = slot.get.toString(); + } + mXmppConnectionService.getFileBackend().updateFileParams(message, get); + mXmppConnectionService.getFileBackend().updateMediaScanner(file); + finish(); + if (!message.isPrivateMessage()) { + message.setCounterpart(message.getConversation().getJid().asBareJid()); + } + mXmppConnectionService.resendMessage(message, delayed); + } else { + Log.d(Config.LOGTAG, "http upload failed because response code was " + code); + fail("http upload failed because response code was " + code); + } + } + }); + } - this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); - message.resetFileParams(); - this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() { - @Override - public void success(SlotRequester.Slot slot) { - if (!cancelled) { - HttpUploadConnection.this.slot = slot; - EXECUTOR.execute(HttpUploadConnection.this::upload); - } - } + public Message getMessage() { + return message; + } - @Override - public void failure(String message) { - fail(message); - } - }); - message.setTransferable(this); - mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); - } - - private void upload() { - OutputStream os = null; - InputStream fileInputStream = null; - HttpURLConnection connection = null; - final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread()); - try { - fileInputStream = new FileInputStream(file); - final String slotHostname = slot.getPutUrl().getHost(); - final boolean onionSlot = slotHostname != null && slotHostname.endsWith(".onion"); - final int expectedFileSize = (int) file.getExpectedSize(); - final int readTimeout = (expectedFileSize / 2048) + Config.SOCKET_TIMEOUT; //assuming a minimum transfer speed of 16kbit/s - wakeLock.acquire(readTimeout); - Log.d(Config.LOGTAG, "uploading to " + slot.getPutUrl().toString()+ " w/ read timeout of "+readTimeout+"s"); - - if (mUseTor || message.getConversation().getAccount().isOnion() || onionSlot) { - connection = (HttpURLConnection) slot.getPutUrl().openConnection(HttpConnectionManager.getProxy()); - } else { - connection = (HttpURLConnection) slot.getPutUrl().openConnection(); - } - if (connection instanceof HttpsURLConnection) { - mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true); - } - connection.setUseCaches(false); - connection.setRequestMethod("PUT"); - connection.setFixedLengthStreamingMode(expectedFileSize); - connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getUserAgent()); - if(slot.getHeaders() != null) { - for(HashMap.Entry entry : slot.getHeaders().entrySet()) { - connection.setRequestProperty(entry.getKey(),entry.getValue()); - } - } - connection.setDoOutput(true); - connection.setDoInput(true); - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(readTimeout * 1000); - connection.connect(); - final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream); - os = connection.getOutputStream(); - transmitted = 0; - int count; - byte[] buffer = new byte[4096]; - while (((count = innerInputStream.read(buffer)) != -1) && !cancelled) { - transmitted += count; - os.write(buffer, 0, count); - mHttpConnectionManager.updateConversationUi(false); - } - os.flush(); - os.close(); - int code = connection.getResponseCode(); - InputStream is = connection.getErrorStream(); - if (is != null) { - try (Scanner scanner = new Scanner(is)) { - scanner.useDelimiter("\\Z"); - Log.d(Config.LOGTAG, "body: " + scanner.next()); - } - } - if (code == 200 || code == 201) { - Log.d(Config.LOGTAG, "finished uploading file"); - final URL get; - if (key != null) { - if (method == Method.P1_S3) { - get = new URL(slot.getGetUrl().toString()+"#"+CryptoHelper.bytesToHex(key)); - } else { - get = CryptoHelper.toAesGcmUrl(new URL(slot.getGetUrl().toString() + "#" + CryptoHelper.bytesToHex(key))); - } - } else { - get = slot.getGetUrl(); - } - mXmppConnectionService.getFileBackend().updateFileParams(message, get); - mXmppConnectionService.getFileBackend().updateMediaScanner(file); - finish(); - if (!message.isPrivateMessage()) { - message.setCounterpart(message.getConversation().getJid().asBareJid()); - } - mXmppConnectionService.resendMessage(message, delayed); - } else { - Log.d(Config.LOGTAG,"http upload failed because response code was "+code); - fail("http upload failed because response code was "+code); - } - } catch (Exception e) { - e.printStackTrace(); - Log.d(Config.LOGTAG,"http upload failed "+e.getMessage()); - fail(e.getMessage()); - } finally { - FileBackend.close(fileInputStream); - FileBackend.close(os); - if (connection != null) { - connection.disconnect(); - } - WakeLockHelper.release(wakeLock); - } - } - - public Message getMessage() { - return message; - } -} + @Override + public void onProgress(final long progress) { + this.transmitted = progress; + mHttpConnectionManager.updateConversationUi(false); + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/Method.java b/src/main/java/eu/siacs/conversations/http/Method.java index 2f731995b..47dae2b30 100644 --- a/src/main/java/eu/siacs/conversations/http/Method.java +++ b/src/main/java/eu/siacs/conversations/http/Method.java @@ -33,7 +33,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xmpp.XmppConnection; public enum Method { - P1_S3, HTTP_UPLOAD, HTTP_UPLOAD_LEGACY; + HTTP_UPLOAD, HTTP_UPLOAD_LEGACY; public static Method determine(Account account) { XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures(); @@ -44,8 +44,6 @@ public enum Method { return HTTP_UPLOAD_LEGACY; } else if (features.httpUpload(0)) { return HTTP_UPLOAD; - } else if (features.p1S3FileTransfer()) { - return P1_S3; } else { return HTTP_UPLOAD; } diff --git a/src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java b/src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java deleted file mode 100644 index 2ba92180c..000000000 --- a/src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.http; - -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; - -import eu.siacs.conversations.xml.Element; - -public class P1S3UrlStreamHandler extends URLStreamHandler { - - public static final String PROTOCOL_NAME = "p1s3"; - - @Override - protected URLConnection openConnection(URL url) { - throw new IllegalStateException("Unable to open connection with stub protocol"); - } - - public static URL of(String fileId, String filename) throws MalformedURLException { - if (fileId == null || filename == null) { - throw new MalformedURLException("Paramaters must not be null"); - } - return new URL(PROTOCOL_NAME+"://" + fileId + "/" + filename); - } - - public static URL of(Element x) { - try { - return of(x.getAttribute("fileid"),x.getAttribute("name")); - } catch (MalformedURLException e) { - return null; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/http/SlotRequester.java b/src/main/java/eu/siacs/conversations/http/SlotRequester.java index b52bab39a..30a484693 100644 --- a/src/main/java/eu/siacs/conversations/http/SlotRequester.java +++ b/src/main/java/eu/siacs/conversations/http/SlotRequester.java @@ -31,9 +31,9 @@ package eu.siacs.conversations.http; import android.util.Log; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.HashMap; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; @@ -44,147 +44,114 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import okhttp3.Headers; +import okhttp3.HttpUrl; public class SlotRequester { - private final XmppConnectionService service; + private final XmppConnectionService service; - public SlotRequester(XmppConnectionService service) { - this.service = service; - } + public SlotRequester(XmppConnectionService service) { + this.service = service; + } - public void request(Method method, Account account, DownloadableFile file, String mime, String md5, OnSlotRequested callback) { - if (method == Method.HTTP_UPLOAD) { - Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD); - requestHttpUpload(account, host, file, mime, callback); - } else if (method == Method.HTTP_UPLOAD_LEGACY) { - Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY); - requestHttpUploadLegacy(account, host, file, mime, callback); - } else { - requestP1S3(account, account.getDomain(), file.getName(), md5, callback); - } - } + public void request(Method method, Account account, DownloadableFile file, String mime, OnSlotRequested callback) { + if (method == Method.HTTP_UPLOAD_LEGACY) { + final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY); + requestHttpUploadLegacy(account, host, file, mime, callback); + } else { + final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD); + requestHttpUpload(account, host, file, mime, callback); + } + } - private void requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) { - IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY); - if (slotElement != null) { - try { - final String putUrl = slotElement.findChildContent("put"); - final String getUrl = slotElement.findChildContent("get"); - if (getUrl != null && putUrl != null) { - Slot slot = new Slot(new URL(putUrl)); - slot.getUrl = new URL(getUrl); - slot.headers = new HashMap<>(); - slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime); - callback.success(slot); - return; - } - } catch (MalformedURLException e) { - //fall through - } - } - } - Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet); - callback.failure(IqParser.extractErrorMessage(packet)); - }); + private void requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) { + IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); + service.sendIqPacket(account, request, (a, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY); + if (slotElement != null) { + try { + final String putUrl = slotElement.findChildContent("put"); + final String getUrl = slotElement.findChildContent("get"); + if (getUrl != null && putUrl != null) { + final Slot slot = new Slot( + HttpUrl.get(putUrl), + HttpUrl.get(getUrl), + Headers.of("Content-Type", mime == null ? "application/octet-stream" : mime) + ); + callback.success(slot); + return; + } + } catch (IllegalArgumentException e) { + //fall through + } + } + } + Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet); + callback.failure(IqParser.extractErrorMessage(packet)); + }); - } + } - private void requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) { - IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD); - if (slotElement != null) { - try { - final Element put = slotElement.findChild("put"); - final Element get = slotElement.findChild("get"); - final String putUrl = put == null ? null : put.getAttribute("url"); - final String getUrl = get == null ? null : get.getAttribute("url"); - if (getUrl != null && putUrl != null) { - Slot slot = new Slot(new URL(putUrl)); - slot.getUrl = new URL(getUrl); - slot.headers = new HashMap<>(); - for (Element child : put.getChildren()) { - if ("header".equals(child.getName())) { - final String name = child.getAttribute("name"); - final String value = child.getContent(); - if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) { - slot.headers.put(name, value.trim()); - } - } - } - slot.headers.put("Content-Type", mime == null ? "application/octet-stream" : mime); - callback.success(slot); - return; - } - } catch (MalformedURLException e) { - //fall through - } - } - } - Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet); - callback.failure(IqParser.extractErrorMessage(packet)); - }); + private void requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime, OnSlotRequested callback) { + IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime); + service.sendIqPacket(account, request, (a, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD); + if (slotElement != null) { + try { + final Element put = slotElement.findChild("put"); + final Element get = slotElement.findChild("get"); + final String putUrl = put == null ? null : put.getAttribute("url"); + final String getUrl = get == null ? null : get.getAttribute("url"); + if (getUrl != null && putUrl != null) { + final ImmutableMap.Builder headers = new ImmutableMap.Builder<>(); + for (Element child : put.getChildren()) { + if ("header".equals(child.getName())) { + final String name = child.getAttribute("name"); + final String value = child.getContent(); + if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) { + headers.put(name, value.trim()); + } + } + } + headers.put("Content-Type", mime == null ? "application/octet-stream" : mime); + final Slot slot = new Slot(HttpUrl.get(putUrl), HttpUrl.get(getUrl), headers.build()); + callback.success(slot); + return; + } + } catch (IllegalArgumentException e) { + //fall through + } + } + } + Log.d(Config.LOGTAG, account.getJid().toString() + ": invalid response to slot request " + packet); + callback.failure(IqParser.extractErrorMessage(packet)); + }); - } + } - private void requestP1S3(final Account account, Jid host, String filename, String md5, OnSlotRequested callback) { - IqPacket request = service.getIqGenerator().requestP1S3Slot(host, md5); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - String putUrl = packet.query(Namespace.P1_S3_FILE_TRANSFER).getAttribute("upload"); - String id = packet.query().getAttribute("fileid"); - try { - if (putUrl != null && id != null) { - Slot slot = new Slot(new URL(putUrl)); - slot.getUrl = P1S3UrlStreamHandler.of(id, filename); - slot.headers = new HashMap<>(); - slot.headers.put("Content-MD5", md5); - slot.headers.put("Content-Type", " "); //required to force it to empty. otherwise library will set something - callback.success(slot); - return; - } - } catch (MalformedURLException e) { - //fall through; - } - } - callback.failure("unable to request slot"); - }); - Log.d(Config.LOGTAG, "requesting slot with p1. md5=" + md5); - } + public interface OnSlotRequested { + void success(Slot slot); + void failure(String message); + } + public static class Slot { + public final HttpUrl put; + public final HttpUrl get; + public final Headers headers; - public interface OnSlotRequested { + private Slot(HttpUrl put, HttpUrl get, Headers headers) { + this.put = put; + this.get = get; + this.headers = headers; + } - void success(Slot slot); - - void failure(String message); - - } - - public static class Slot { - private final URL putUrl; - private URL getUrl; - private HashMap headers; - - private Slot(URL putUrl) { - this.putUrl = putUrl; - } - - public URL getPutUrl() { - return putUrl; - } - - public URL getGetUrl() { - return getUrl; - } - - public HashMap getHeaders() { - return headers; - } - } + private Slot(HttpUrl put, HttpUrl getUrl, Map headers) { + this.put = put; + this.get = getUrl; + this.headers = Headers.of(headers); + } + } } diff --git a/src/main/java/eu/siacs/conversations/http/URL.java b/src/main/java/eu/siacs/conversations/http/URL.java new file mode 100644 index 000000000..921a29817 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/URL.java @@ -0,0 +1,34 @@ +package eu.siacs.conversations.http; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; + +import eu.siacs.conversations.http.AesGcmURL; +import okhttp3.HttpUrl; + +public class URL { + + public static final List WELL_KNOWN_SCHEMES = Arrays.asList("http", "https", AesGcmURL.PROTOCOL_NAME); + + + public static String tryParse(String url) { + final URI uri; + try { + uri = new URI(url); + } catch (URISyntaxException e) { + return null; + } + if (WELL_KNOWN_SCHEMES.contains(uri.getScheme())) { + return uri.toString(); + } else { + return null; + } + } + + public static HttpUrl stripFragment(final HttpUrl url) { + return url.newBuilder().fragment(null).build(); + } + +} diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index dae71abda..600e4ea84 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -33,7 +33,6 @@ import eu.siacs.conversations.entities.ReadByMarker; import eu.siacs.conversations.entities.ReceiptRequest; import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.http.HttpConnectionManager; -import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; @@ -408,8 +407,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0"); final Element oob = packet.findChild("x", Namespace.OOB); - final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER); - final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3); final String oobUrl = oob != null ? oob.findChildContent("url") : null; final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id"); final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); @@ -464,7 +461,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } - if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) { + if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) { final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString()); final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false); final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI; @@ -504,13 +501,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } final Message message; - if (xP1S3url != null) { - message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status); - message.setOob(true); - if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) { - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - } - } else if (pgpEncrypted != null && Config.supportOpenPgp()) { + if (pgpEncrypted != null && Config.supportOpenPgp()) { message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); } else if (axolotlEncrypted != null && Config.supportOmemo()) { Jid origin; diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 690caaafd..b847ca169 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -416,9 +416,9 @@ public class FileBackend { } } - public static void updateFileParams(Message message, URL url, long size) { + public static void updateFileParams(Message message, String url, long size) { final StringBuilder body = new StringBuilder(); - body.append(url.toString()).append('|').append(size); + body.append(url).append('|').append(size); message.setBody(body.toString()); } @@ -1305,7 +1305,7 @@ public class FileBackend { updateFileParams(message, null); } - public void updateFileParams(Message message, URL url) { + public void updateFileParams(Message message, String url) { DownloadableFile file = getFile(message); final String mime = file.getMimeType(); final boolean privateMessage = message.isPrivateMessage(); @@ -1315,7 +1315,7 @@ public class FileBackend { final boolean pdf = "application/pdf".equals(mime); final StringBuilder body = new StringBuilder(); if (url != null) { - body.append(url.toString()); + body.append(url); } body.append('|').append(file.getSize()); if (image || video || (pdf && Compatibility.runsTwentyOne())) { diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index dcf8848bf..3024a1045 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.services; import android.content.Context; +import android.os.FileUtils; import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; @@ -13,8 +14,10 @@ import org.bouncycastle.crypto.modes.GCMBlockCipher; import org.bouncycastle.crypto.params.AEADParameters; import org.bouncycastle.crypto.params.KeyParameter; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; @@ -23,12 +26,21 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Nullable; import javax.crypto.NoSuchPaddingException; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.utils.Compatibility; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.Buffer; +import okio.BufferedSink; +import okio.ForwardingSink; +import okio.Okio; +import okio.Sink; +import okio.Source; import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS; @@ -42,7 +54,7 @@ public class AbstractConnectionManager { this.mXmppConnectionService = service; } - public static InputStream upgrade(DownloadableFile file, InputStream is) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, NoSuchProviderException { + public static InputStream upgrade(DownloadableFile file, InputStream is) { if (file.getKey() != null && file.getIv() != null) { AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); @@ -52,6 +64,43 @@ public class AbstractConnectionManager { } } + + //For progress tracking see: + //https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java + + public static RequestBody requestBody(final DownloadableFile file, final ProgressListener progressListener) { + return new RequestBody() { + + @Override + public long contentLength() { + return file.getSize() + (file.getKey() != null ? 16 : 0); + } + + @Nullable + @Override + public MediaType contentType() { + return MediaType.parse(file.getMimeType()); + } + + @Override + public void writeTo(final BufferedSink sink) throws IOException { + long transmitted = 0; + try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) { + long read; + while ((read = source.read(sink.buffer(), 8196)) != -1) { + transmitted += read; + sink.flush(); + progressListener.onProgress(transmitted); + } + } + } + }; + } + + public interface ProgressListener { + void onProgress(long progress); + } + public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) { FileOutputStream os; try { @@ -121,6 +170,7 @@ public class AbstractConnectionManager { } public static Extension of(String path) { + //TODO accept List pathSegments final int pos = path.lastIndexOf('/'); final String filename = path.substring(pos + 1).toLowerCase(); final String[] parts = filename.split("\\."); diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index a706dfb30..4c73e04dc 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -51,13 +51,8 @@ public class ChannelDiscoveryService { void initializeMuclumbusService() { final OkHttpClient.Builder builder = new OkHttpClient.Builder(); - if (service.useTorToConnect()) { - try { - builder.proxy(HttpConnectionManager.getProxy()); - } catch (IOException e) { - throw new RuntimeException("Unable to use Tor proxy", e); - } + builder.proxy(HttpConnectionManager.getProxy()); } Retrofit retrofit = new Retrofit.Builder() .client(builder.build()) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 146fc8bf1..6611a52be 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -104,7 +104,6 @@ import eu.siacs.conversations.generator.AbstractGenerator; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.MessageGenerator; import eu.siacs.conversations.generator.PresenceGenerator; -import eu.siacs.conversations.http.CustomURLStreamHandlerFactory; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.parser.IqParser; @@ -183,10 +182,6 @@ public class XmppConnectionService extends Service { private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; - static { - URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory()); - } - public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1); private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding"); private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression"); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 82b9eb143..8fb894fa4 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1605,7 +1605,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void createNewConnection(final Message message) { - if (!activity.xmppConnectionService.getHttpConnectionManager().checkConnection(message)) { + if (!activity.xmppConnectionService.hasInternetConnection()) { Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); return; } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index e7c042c06..5d08b01bb 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1071,9 +1071,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { this.binding.serverInfoHttpUpload.setText(R.string.server_info_available); } - } else if (features.p1S3FileTransfer()) { - this.binding.serverInfoHttpUploadDescription.setText(R.string.p1_s3_filetransfer); - this.binding.serverInfoHttpUpload.setText(R.string.server_info_available); } else { this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable); } diff --git a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java index 3e830c7c0..4ae3a8fde 100644 --- a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java @@ -98,11 +98,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca config.load(ctx, getPreferences()); config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE); if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) { - try { - config.setHttpProxy(HttpConnectionManager.getProxy()); - } catch (IOException e) { - throw new RuntimeException("Unable to configure proxy"); - } + config.setHttpProxy(HttpConnectionManager.getProxy()); } } 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 f7b76e78f..3fdcb9ad0 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Typeface; +import android.net.Uri; import android.preference.PreferenceManager; import android.text.Spannable; import android.text.SpannableString; @@ -31,6 +32,7 @@ import androidx.core.content.ContextCompat; import com.google.common.base.Strings; +import java.net.URI; import java.net.URL; import java.util.List; import java.util.Locale; @@ -48,7 +50,6 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message.FileParams; import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.Transferable; -import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.NotificationService; @@ -800,21 +801,13 @@ public class MessageAdapter extends ArrayAdapter { displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground); } else if (message.treatAsDownloadable()) { try { - URL url = new URL(message.getBody()); - if (P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(url.getProtocol())) { - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize, - UIHelper.getFileDescriptionString(activity, message)), - darkBackground); - } else { + final URI uri = new URI(message.getBody()); displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize_on_host, UIHelper.getFileDescriptionString(activity, message), - url.getHost()), + uri.getHost()), darkBackground); - } } catch (Exception e) { displayDownloadableMessage(viewHolder, message, @@ -903,10 +896,6 @@ public class MessageAdapter extends ArrayAdapter { this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); } - public interface OnQuoteListener { - void onQuote(String text); - } - public interface OnContactPictureClicked { void onContactPictureClicked(Message message); } diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index fb92f47bf..2470a428f 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -94,10 +94,10 @@ public class ShareUtil { url = message.getBody(); } else if (message.hasFileOnRemoteHost()) { resId = R.string.file_url; - url = message.getFileParams().url.toString(); + url = message.getFileParams().url; } else { final Message.FileParams fileParams = message.getFileParams(); - url = (fileParams != null && fileParams.url != null) ? fileParams.url.toString() : message.getBody().trim(); + url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim(); resId = R.string.file_url; } if (activity.copyTextToClipboard(url, resId)) { diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 3174c4198..5119ec501 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -31,7 +31,6 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.http.AesGcmURLStreamHandler; import eu.siacs.conversations.xmpp.Jid; public final class CryptoHelper { @@ -278,28 +277,6 @@ public final class CryptoHelper { } } - public static URL toAesGcmUrl(URL url) { - if (!url.getProtocol().equalsIgnoreCase("https")) { - return url; - } - try { - return new URL(AesGcmURLStreamHandler.PROTOCOL_NAME + url.toString().substring(url.getProtocol().length())); - } catch (MalformedURLException e) { - return url; - } - } - - public static URL toHttpsUrl(URL url) { - if (!url.getProtocol().equalsIgnoreCase(AesGcmURLStreamHandler.PROTOCOL_NAME)) { - return url; - } - try { - return new URL("https" + url.toString().substring(url.getProtocol().length())); - } catch (MalformedURLException e) { - return url; - } - } - public static boolean isPgpEncryptedUrl(String url) { if (url == null) { return false; diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index d8b991894..ce58797a8 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -29,88 +29,94 @@ package eu.siacs.conversations.utils; +import android.net.Uri; + import com.google.common.base.Strings; -import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; import java.util.regex.Pattern; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.http.AesGcmURLStreamHandler; -import eu.siacs.conversations.http.P1S3UrlStreamHandler; +import eu.siacs.conversations.http.AesGcmURL; +import eu.siacs.conversations.http.URL; public class MessageUtils { - private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}"); + private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}"); - private static final String EMPTY_STRING = ""; + private static final String EMPTY_STRING = ""; - public static String prepareQuote(Message message) { - final StringBuilder builder = new StringBuilder(); - final String body; - if (message.hasMeCommand()) { - final String nick; - if (message.getStatus() == Message.STATUS_RECEIVED) { - if (message.getConversation().getMode() == Conversational.MODE_MULTI) { - nick = Strings.nullToEmpty(message.getCounterpart().getResource()); - } else { - nick = message.getContact().getPublicDisplayName(); - } - } else { - nick = UIHelper.getMessageDisplayName(message); - } - body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length()); - } else { - body = message.getMergedBody().toString(); + public static String prepareQuote(Message message) { + final StringBuilder builder = new StringBuilder(); + final String body; + if (message.hasMeCommand()) { + final String nick; + if (message.getStatus() == Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversational.MODE_MULTI) { + nick = Strings.nullToEmpty(message.getCounterpart().getResource()); + } else { + nick = message.getContact().getPublicDisplayName(); + } + } else { + nick = UIHelper.getMessageDisplayName(message); + } + body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length()); + } else { + body = message.getMergedBody().toString(); } - for (String line : body.split("\n")) { - if (line.length() <= 0) { - continue; - } - final char c = line.charAt(0); - if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0) - || (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) { - continue; - } - if (builder.length() != 0) { - builder.append('\n'); - } - builder.append(line.trim()); - } - return builder.toString(); - } + for (String line : body.split("\n")) { + if (line.length() <= 0) { + continue; + } + final char c = line.charAt(0); + if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0) + || (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) { + continue; + } + if (builder.length() != 0) { + builder.append('\n'); + } + builder.append(line.trim()); + } + return builder.toString(); + } - public static boolean treatAsDownloadable(final String body, final boolean oob) { - try { - final String[] lines = body.split("\n"); - if (lines.length == 0) { - return false; - } - for (String line : lines) { - if (line.contains("\\s+")) { - return false; - } - } - final URL url = new URL(lines[0]); - final String ref = url.getRef(); - final String protocol = url.getProtocol(); - final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches(); - final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:"); - final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri); - final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol) || P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol); - final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1; - return validAesGcm || validOob; - } catch (MalformedURLException e) { - return false; - } - } + public static boolean treatAsDownloadable(final String body, final boolean oob) { + final String[] lines = body.split("\n"); + if (lines.length == 0) { + return false; + } + for (final String line : lines) { + if (line.contains("\\s+")) { + return false; + } + } + final URI uri; + try { + uri = new URI(lines[0]); + } catch (final URISyntaxException e) { + return false; + } + if (!URL.WELL_KNOWN_SCHEMES.contains(uri.getScheme())) { + return false; + } + final String ref = uri.getFragment(); + final String protocol = uri.getScheme(); + final boolean encrypted = ref != null && AesGcmURL.IV_KEY.matcher(ref).matches(); + final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:"); + final boolean validAesGcm = AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri); + final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol); + final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1; + return validAesGcm || validOob; + } - public static String filterLtrRtl(String body) { - return LTR_RTL.matcher(body).replaceFirst(EMPTY_STRING); - } + public static String filterLtrRtl(String body) { + return LTR_RTL.matcher(body).replaceFirst(EMPTY_STRING); + } - public static boolean unInitiatedButKnownSize(Message message) { - return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null; - } + public static boolean unInitiatedButKnownSize(Message message) { + return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size > 0 && message.getFileParams().url != null; + } } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 29b4bf395..b0c4fe85c 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -23,7 +23,6 @@ public final class Namespace { public static final String NICK = "http://jabber.org/protocol/nick"; public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; - public static final String P1_S3_FILE_TRANSFER = "p1:s3filetransfer"; public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index b188824ae..355a6b847 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1921,10 +1921,6 @@ public class XmppConnection implements Runnable { this.blockListRequested = value; } - public boolean p1S3FileTransfer() { - return hasDiscoFeature(account.getDomain(), Namespace.P1_S3_FILE_TRANSFER); - } - public boolean httpUpload(long filesize) { if (Config.DISABLE_HTTP_UPLOAD) { return false;