Detailed changes
@@ -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')
@@ -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);
@@ -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();
}
@@ -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();
}
@@ -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() {
@@ -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;
}
@@ -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<Edit> 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<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
-
- private Boolean isGeoUri = null;
- private Boolean isEmojisOnly = null;
- private Boolean treatAsDownloadable = null;
- private FileParams fileParams = null;
- private List<MucOptions.User> counterparts;
- private WeakReference<MucOptions.User> 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<ReadByMarker> 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<ReadByMarker> 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<ReadByMarker> 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<MucOptions.User> counterparts) {
- this.counterparts = counterparts;
- }
-
- public List<MucOptions.User> 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<Edit> 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<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
+
+ private Boolean isGeoUri = null;
+ private Boolean isEmojisOnly = null;
+ private Boolean treatAsDownloadable = null;
+ private FileParams fileParams = null;
+ private List<MucOptions.User> counterparts;
+ private WeakReference<MucOptions.User> 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<ReadByMarker> 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<ReadByMarker> 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<ReadByMarker> 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<MucOptions.User> counterparts) {
+ this.counterparts = counterparts;
+ }
+
+ public List<MucOptions.User> 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;
+ }
}
@@ -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) {
@@ -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<String> namespaces, int type) {
+ public MessagePacket received(Account account, final Jid from, final String id, ArrayList<String> namespaces, int type) {
final MessagePacket receivedPacket = new MessagePacket();
receivedPacket.setType(type);
receivedPacket.setTo(from);
@@ -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);
+ }
+ }
+
+}
@@ -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();
- }
-}
@@ -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;
- }
- }
-}
@@ -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) {
}
}
@@ -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;
-
- 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 OkHttpClient client = mHttpConnectionManager.buildHttpClient(
+ mUrl,
+ message.getConversation().getAccount(),
+ interactive
+ );
+
+ 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);
@@ -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 org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.List;
-import java.util.Scanner;
-
-import javax.net.ssl.HttpsURLConnection;
+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 static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
-
-public class HttpUploadConnection implements Transferable {
-
- static final List<String> 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 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;
-
- 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 int getStatus() {
- return STATUS_UPLOADING;
- }
-
- @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 void cancel() {
- this.cancelled = true;
- }
-
- 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);
- }
-
- 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);
- }
-
- final String md5;
-
- 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;
- }
-
- 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);
- }
- }
-
- @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<String,String> 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;
- }
-}
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener {
+
+ static final List<String> 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 long transmitted = 0;
+ private Call mostRecentCall;
+
+ 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 int getStatus() {
+ return STATUS_UPLOADING;
+ }
+
+ @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 void cancel() {
+ final Call call = this.mostRecentCall;
+ if (call != null && !call.isCanceled()) {
+ call.cancel();
+ }
+ }
+
+ 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 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();
+ }
+
+ @Override
+ public void failure(String message) {
+ fail(message);
+ }
+ });
+ message.setTransferable(this);
+ mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
+ }
+
+ 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());
+ }
+
+ @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);
+ }
+ }
+ });
+ }
+
+ public Message getMessage() {
+ return message;
+ }
+
+ @Override
+ public void onProgress(final long progress) {
+ this.transmitted = progress;
+ mHttpConnectionManager.updateConversationUi(false);
+ }
+}
@@ -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;
}
@@ -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;
- }
- }
-}
@@ -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;
-
- 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);
- }
- }
-
- 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 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 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 {
- private final URL putUrl;
- private URL getUrl;
- private HashMap<String, String> headers;
-
- private Slot(URL putUrl) {
- this.putUrl = putUrl;
- }
-
- public URL getPutUrl() {
- return putUrl;
- }
-
- public URL getGetUrl() {
- return getUrl;
- }
-
- public HashMap<String, String> getHeaders() {
- return headers;
- }
- }
+ private final XmppConnectionService service;
+
+ public SlotRequester(XmppConnectionService service) {
+ this.service = service;
+ }
+
+ 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) {
+ 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) {
+ 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<String, String> 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));
+ });
+
+ }
+
+ 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;
+
+ private Slot(HttpUrl put, HttpUrl get, Headers headers) {
+ this.put = put;
+ this.get = get;
+ this.headers = headers;
+ }
+
+ private Slot(HttpUrl put, HttpUrl getUrl, Map<String, String> headers) {
+ this.put = put;
+ this.get = getUrl;
+ this.headers = Headers.of(headers);
+ }
+ }
}
@@ -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<String> 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();
+ }
+
+}
@@ -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;
@@ -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())) {
@@ -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<String> pathSegments
final int pos = path.lastIndexOf('/');
final String filename = path.substring(pos + 1).toLowerCase();
final String[] parts = filename.split("\\.");
@@ -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())
@@ -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");
@@ -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;
}
@@ -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);
}
@@ -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());
}
}
@@ -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<Message> {
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<Message> {
this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
}
- public interface OnQuoteListener {
- void onQuote(String text);
- }
-
public interface OnContactPictureClicked {
void onContactPictureClicked(Message message);
}
@@ -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)) {
@@ -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;
@@ -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());
}
- 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();
- }
+ 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;
+ }
}
@@ -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";
@@ -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;