migrate to OkHttp instead of HttpUrlConnection

Daniel Gultsch created

OkHttp gives us more fine grained control over the HTTP library and frees us from any platform bugs

Change summary

build.gradle                                                                 |    1 
src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java        |    2 
src/main/java/eu/siacs/conversations/crypto/PgpEngine.java                   |    2 
src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java      |    2 
src/main/java/eu/siacs/conversations/entities/Account.java                   |    2 
src/main/java/eu/siacs/conversations/entities/Conversation.java              |    2 
src/main/java/eu/siacs/conversations/entities/Message.java                   | 1934 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java              |   14 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java         |   31 
src/main/java/eu/siacs/conversations/http/AesGcmURL.java                     |   41 
src/main/java/eu/siacs/conversations/http/AesGcmURLStreamHandler.java        |   23 
src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java |   18 
src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java         |   44 
src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java        |  245 
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java          |  414 
src/main/java/eu/siacs/conversations/http/Method.java                        |    4 
src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java          |   62 
src/main/java/eu/siacs/conversations/http/SlotRequester.java                 |  253 
src/main/java/eu/siacs/conversations/http/URL.java                           |   34 
src/main/java/eu/siacs/conversations/parser/MessageParser.java               |   13 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java            |    8 
src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java |   52 
src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java   |    7 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java     |    5 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java            |    2 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java             |    3 
src/main/java/eu/siacs/conversations/ui/LocationActivity.java                |    6 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java          |   19 
src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java                  |    4 
src/main/java/eu/siacs/conversations/utils/CryptoHelper.java                 |   23 
src/main/java/eu/siacs/conversations/utils/MessageUtils.java                 |  144 
src/main/java/eu/siacs/conversations/xml/Namespace.java                      |    1 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                |    4 
33 files changed, 1,594 insertions(+), 1,825 deletions(-)

Detailed changes

build.gradle 🔗

@@ -74,6 +74,7 @@ dependencies {
 
     implementation "com.squareup.retrofit2:retrofit:2.9.0"
     implementation "com.squareup.retrofit2:converter-gson:2.9.0"
+    //implementation "com.squareup.okhttp3:logging-interceptor:3.14.9"
     implementation 'com.google.guava:guava:30.1-android'
     quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18'
     implementation fileTree(include: ['libwebrtc-m89.aar'], dir: 'libs')

src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java 🔗

@@ -209,7 +209,7 @@ public class PgpDecryptionService {
 									message.setRelativeFilePath(path);
 								}
 							}
-							URL url = message.getFileParams().url;
+							final String url = message.getFileParams().url;
 							mXmppConnectionService.getFileBackend().updateFileParams(message, url);
 							message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 							mXmppConnectionService.updateMessage(message);

src/main/java/eu/siacs/conversations/crypto/PgpEngine.java 🔗

@@ -75,7 +75,7 @@ public class PgpEngine {
             params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
             String body;
             if (message.hasFileOnRemoteHost()) {
-                body = message.getFileParams().url.toString();
+                body = message.getFileParams().url;
             } else {
                 body = message.getBody();
             }

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -1169,7 +1169,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
         final String content;
         if (message.hasFileOnRemoteHost()) {
-            content = message.getFileParams().url.toString();
+            content = message.getFileParams().url;
         } else {
             content = message.getBody();
         }

src/main/java/eu/siacs/conversations/entities/Account.java 🔗

@@ -147,7 +147,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
     }
 
     public boolean httpUploadAvailable(long filesize) {
-        return xmppConnection != null && (xmppConnection.getFeatures().httpUpload(filesize) || xmppConnection.getFeatures().p1S3FileTransfer());
+        return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
     }
 
     public boolean httpUploadAvailable() {

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -788,7 +788,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
                     String otherBody;
                     if (message.hasFileOnRemoteHost()) {
-                        otherBody = message.getFileParams().url.toString();
+                        otherBody = message.getFileParams().url;
                     } else {
                         otherBody = message.body;
                     }

src/main/java/eu/siacs/conversations/entities/Message.java 🔗

@@ -12,8 +12,6 @@ import com.google.common.collect.ImmutableSet;
 import org.json.JSONException;
 
 import java.lang.ref.WeakReference;
-import java.net.MalformedURLException;
-import java.net.URL;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -22,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
+import eu.siacs.conversations.http.URL;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.ui.util.PresenceSelector;
 import eu.siacs.conversations.utils.CryptoHelper;
@@ -32,973 +31,966 @@ import eu.siacs.conversations.utils.MimeUtils;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.Jid;
 
-public class Message extends AbstractEntity implements AvatarService.Avatarable  {
-
-	public static final String TABLENAME = "messages";
-
-	public static final int STATUS_RECEIVED = 0;
-	public static final int STATUS_UNSEND = 1;
-	public static final int STATUS_SEND = 2;
-	public static final int STATUS_SEND_FAILED = 3;
-	public static final int STATUS_WAITING = 5;
-	public static final int STATUS_OFFERED = 6;
-	public static final int STATUS_SEND_RECEIVED = 7;
-	public static final int STATUS_SEND_DISPLAYED = 8;
-
-	public static final int ENCRYPTION_NONE = 0;
-	public static final int ENCRYPTION_PGP = 1;
-	public static final int ENCRYPTION_OTR = 2;
-	public static final int ENCRYPTION_DECRYPTED = 3;
-	public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
-	public static final int ENCRYPTION_AXOLOTL = 5;
-	public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
-	public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
-
-	public static final int TYPE_TEXT = 0;
-	public static final int TYPE_IMAGE = 1;
-	public static final int TYPE_FILE = 2;
-	public static final int TYPE_STATUS = 3;
-	public static final int TYPE_PRIVATE = 4;
-	public static final int TYPE_PRIVATE_FILE = 5;
-	public static final int TYPE_RTP_SESSION = 6;
-
-	public static final String CONVERSATION = "conversationUuid";
-	public static final String COUNTERPART = "counterpart";
-	public static final String TRUE_COUNTERPART = "trueCounterpart";
-	public static final String BODY = "body";
-	public static final String BODY_LANGUAGE = "bodyLanguage";
-	public static final String TIME_SENT = "timeSent";
-	public static final String ENCRYPTION = "encryption";
-	public static final String STATUS = "status";
-	public static final String TYPE = "type";
-	public static final String CARBON = "carbon";
-	public static final String OOB = "oob";
-	public static final String EDITED = "edited";
-	public static final String REMOTE_MSG_ID = "remoteMsgId";
-	public static final String SERVER_MSG_ID = "serverMsgId";
-	public static final String RELATIVE_FILE_PATH = "relativeFilePath";
-	public static final String FINGERPRINT = "axolotl_fingerprint";
-	public static final String READ = "read";
-	public static final String ERROR_MESSAGE = "errorMsg";
-	public static final String READ_BY_MARKERS = "readByMarkers";
-	public static final String MARKABLE = "markable";
-	public static final String DELETED = "deleted";
-	public static final String ME_COMMAND = "/me ";
-
-	public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
-
-
-	public boolean markable = false;
-	protected String conversationUuid;
-	protected Jid counterpart;
-	protected Jid trueCounterpart;
-	protected String body;
-	protected String encryptedBody;
-	protected long timeSent;
-	protected int encryption;
-	protected int status;
-	protected int type;
-	protected boolean deleted = false;
-	protected boolean carbon = false;
-	protected boolean oob = false;
-	protected List<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;
+    }
 }

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -408,20 +408,6 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public IqPacket requestP1S3Slot(Jid host, String md5) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
-        packet.setTo(host);
-        packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5", md5);
-        return packet;
-    }
-
-    public IqPacket requestP1S3Url(Jid host, String fileId) {
-        IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
-        packet.setTo(host);
-        packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("fileid", fileId);
-        return packet;
-    }
-
     private static String convertFilename(String name) {
         int pos = name.indexOf('.');
         if (pos != -1) {

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -14,7 +14,6 @@ import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.http.P1S3UrlStreamHandler;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
@@ -103,18 +102,9 @@ public class MessageGenerator extends AbstractGenerator {
         MessagePacket packet = preparePacket(message);
         String content;
         if (message.hasFileOnRemoteHost()) {
-            Message.FileParams fileParams = message.getFileParams();
-            final URL url = fileParams.url;
-            if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
-                Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
-                final String file = url.getFile();
-                x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
-                x.setAttribute("fileid", url.getHost());
-                return packet;
-            } else {
-                content = url.toString();
-                packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
-            }
+            final Message.FileParams fileParams = message.getFileParams();
+            content = fileParams.url;
+            packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
         } else {
             content = message.getBody();
         }
@@ -126,16 +116,9 @@ public class MessageGenerator extends AbstractGenerator {
         MessagePacket packet = preparePacket(message);
         if (message.hasFileOnRemoteHost()) {
             Message.FileParams fileParams = message.getFileParams();
-            final URL url = fileParams.url;
-            if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) {
-                Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER);
-                final String file = url.getFile();
-                x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file);
-                x.setAttribute("fileid", url.getHost());
-            } else {
-                packet.setBody(url.toString());
-                packet.addChild("x", Namespace.OOB).addChild("url").setContent(url.toString());
-            }
+            final String url = fileParams.url;
+            packet.setBody(url);
+            packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
         } else {
             if (Config.supportUnencrypted()) {
                 packet.setBody(PGP_FALLBACK_MESSAGE);
@@ -225,7 +208,7 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket received(Account account, final Jid from, final String id,  ArrayList<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);

src/main/java/eu/siacs/conversations/http/AesGcmURL.java 🔗

@@ -0,0 +1,41 @@
+package eu.siacs.conversations.http;
+
+import java.util.regex.Pattern;
+
+import okhttp3.HttpUrl;
+
+public final class AesGcmURL {
+
+    /**
+     * This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors
+     */
+    public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}");
+
+    public static final String PROTOCOL_NAME = "aesgcm";
+
+    private AesGcmURL() {
+
+    }
+
+    public static String toAesGcmUrl(HttpUrl url) {
+        if (url.isHttps()) {
+            return PROTOCOL_NAME + url.toString().substring(5);
+        } else {
+            return url.toString();
+        }
+    }
+
+    public static HttpUrl of(final String url) {
+        final int end = url.indexOf("://");
+        if (end < 0) {
+            throw new IllegalArgumentException("Scheme not found");
+        }
+        final String protocol = url.substring(0, end);
+        if (PROTOCOL_NAME.equals(protocol)) {
+            return HttpUrl.get("https" + url.substring(PROTOCOL_NAME.length()));
+        } else {
+            return HttpUrl.get(url);
+        }
+    }
+
+}

src/main/java/eu/siacs/conversations/http/AesGcmURLStreamHandler.java 🔗

@@ -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();
-    }
-}

src/main/java/eu/siacs/conversations/http/CustomURLStreamHandlerFactory.java 🔗

@@ -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;
-        }
-    }
-}

src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java 🔗

@@ -4,20 +4,19 @@ import android.util.Log;
 
 import org.apache.http.conn.ssl.StrictHostnameVerifier;
 
-import java.io.IOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Proxy;
-import java.net.URL;
+import java.net.UnknownHostException;
 import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.X509TrustManager;
 
@@ -27,6 +26,8 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.services.AbstractConnectionManager;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.TLSSocketFactory;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
 
 public class HttpConnectionManager extends AbstractConnectionManager {
 
@@ -39,8 +40,12 @@ public class HttpConnectionManager extends AbstractConnectionManager {
         super(service);
     }
 
-    public static Proxy getProxy() throws IOException {
-        return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050));
+    public static Proxy getProxy() {
+        try {
+            return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 9050));
+        } catch (final UnknownHostException e) {
+            throw new IllegalStateException(e);
+        }
     }
 
     public void createNewDownloadConnection(Message message) {
@@ -75,15 +80,6 @@ public class HttpConnectionManager extends AbstractConnectionManager {
         }
     }
 
-    public boolean checkConnection(Message message) {
-        final Account account = message.getConversation().getAccount();
-        final URL url = message.getFileParams().url;
-        if (url.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) && account.getStatus() != Account.State.ONLINE) {
-            return false;
-        }
-        return mXmppConnectionService.hasInternetConnection();
-    }
-
     void finishConnection(HttpDownloadConnection connection) {
         synchronized (this.downloadConnections) {
             this.downloadConnections.remove(connection);
@@ -96,7 +92,21 @@ public class HttpConnectionManager extends AbstractConnectionManager {
         }
     }
 
-    void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) {
+    OkHttpClient buildHttpClient(final HttpUrl url, final Account account, boolean interactive) {
+        final String slotHostname = url.host();
+        final boolean onionSlot = slotHostname.endsWith(".onion");
+        final OkHttpClient.Builder builder = new OkHttpClient.Builder();
+        //builder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS));
+        builder.writeTimeout(30, TimeUnit.SECONDS);
+        builder.readTimeout(30, TimeUnit.SECONDS);
+        setupTrustManager(builder, interactive);
+        if (mXmppConnectionService.useTorToConnect() || account.isOnion() || onionSlot) {
+            builder.proxy(HttpConnectionManager.getProxy()).build();
+        }
+        return builder.build();
+    }
+
+    private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) {
         final X509TrustManager trustManager;
         final HostnameVerifier hostnameVerifier = mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier(), interactive);
         if (interactive) {
@@ -106,8 +116,8 @@ public class HttpConnectionManager extends AbstractConnectionManager {
         }
         try {
             final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
-            connection.setSSLSocketFactory(sf);
-            connection.setHostnameVerifier(hostnameVerifier);
+            builder.sslSocketFactory(sf, trustManager);
+            builder.hostnameVerifier(hostnameVerifier);
         } catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
         }
     }

src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java 🔗

@@ -1,29 +1,24 @@
 package eu.siacs.conversations.http;
 
-import android.os.PowerManager;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
 
 import com.google.common.base.Strings;
 import com.google.common.io.ByteStreams;
+import com.google.common.primitives.Longs;
 
-import java.io.BufferedInputStream;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
+import java.util.Locale;
 import java.util.concurrent.CancellationException;
 
-import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLHandshakeException;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.Transferable;
@@ -33,8 +28,11 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.FileWriterException;
 import eu.siacs.conversations.utils.MimeUtils;
-import eu.siacs.conversations.utils.WakeLockHelper;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import okhttp3.Call;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
 
 import static eu.siacs.conversations.http.HttpConnectionManager.EXECUTOR;
 
@@ -44,13 +42,13 @@ public class HttpDownloadConnection implements Transferable {
     private final boolean mUseTor;
     private final HttpConnectionManager mHttpConnectionManager;
     private final XmppConnectionService mXmppConnectionService;
-    private URL mUrl;
+    private HttpUrl mUrl;
     private DownloadableFile file;
     private int mStatus = Transferable.STATUS_UNKNOWN;
     private boolean acceptedAutomatically = false;
     private int mProgress = 0;
     private boolean canceled = false;
-    private Method method = Method.HTTP_UPLOAD;
+    private Call mostRecentCall;
 
     HttpDownloadConnection(Message message, HttpConnectionManager manager) {
         this.message = message;
@@ -88,13 +86,13 @@ public class HttpDownloadConnection implements Transferable {
         try {
             final Message.FileParams fileParams = message.getFileParams();
             if (message.hasFileOnRemoteHost()) {
-                mUrl = CryptoHelper.toHttpsUrl(fileParams.url);
+                mUrl = AesGcmURL.of(fileParams.url);
             } else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
-                mUrl = fileParams.url;
+                mUrl = AesGcmURL.of(fileParams.url);
             } else {
-                mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0]));
+                mUrl = AesGcmURL.of(message.getBody().split("\n")[0]);
             }
-            final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
+            final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
             if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
                 this.message.setEncryption(Message.ENCRYPTION_PGP);
             } else if (message.getEncryption() != Message.ENCRYPTION_OTR
@@ -111,22 +109,22 @@ public class HttpDownloadConnection implements Transferable {
             if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
                 this.message.setEncryption(Message.ENCRYPTION_NONE);
             }
-            method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD;
-            long knownFileSize = message.getFileParams().size;
-            if (knownFileSize > 0 && interactive && method != Method.P1_S3) {
+            //TODO add auth tag size to knownFileSize
+            final long knownFileSize = message.getFileParams().size;
+            if (knownFileSize > 0 && interactive) {
                 this.file.setExpectedSize(knownFileSize);
                 download(true);
             } else {
                 checkFileSize(interactive);
             }
-        } catch (MalformedURLException e) {
+        } catch (final IllegalArgumentException e) {
             this.cancel();
         }
     }
 
     private void setupFile() {
-        final String reference = mUrl.getRef();
-        if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
+        final String reference = mUrl.fragment();
+        if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
             this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
             this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
             Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
@@ -146,6 +144,10 @@ public class HttpDownloadConnection implements Transferable {
     @Override
     public void cancel() {
         this.canceled = true;
+        final Call call = this.mostRecentCall;
+        if (call != null && !call.isCanceled()) {
+            call.cancel();
+        }
         mHttpConnectionManager.finishConnection(this);
         message.setTransferable(null);
         if (message.isFileOrImage()) {
@@ -260,33 +262,7 @@ public class HttpDownloadConnection implements Transferable {
 
         @Override
         public void run() {
-            if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) {
-                retrieveUrl();
-            } else {
-                check();
-            }
-        }
-
-        private void retrieveUrl() {
-            changeStatus(STATUS_CHECKING);
-            final Account account = message.getConversation().getAccount();
-            IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(account.getDomain(), mUrl.getHost());
-            mXmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (a, packet) -> {
-                if (packet.getType() == IqPacket.TYPE.RESULT) {
-                    String download = packet.query().getAttribute("download");
-                    if (download != null) {
-                        try {
-                            mUrl = new URL(download);
-                            check();
-                            return;
-                        } catch (MalformedURLException e) {
-                            //fallthrough
-                        }
-                    }
-                }
-                Log.d(Config.LOGTAG, "unable to retrieve actual download url");
-                retrieveFailed(null);
-            });
+            check();
         }
 
         private void retrieveFailed(@Nullable Exception e) {
@@ -330,46 +306,21 @@ public class HttpDownloadConnection implements Transferable {
         }
 
         private long retrieveFileSize() throws IOException {
+            final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
+                    mUrl,
+                    message.getConversation().getAccount(),
+                    interactive
+            );
+            final Request request = new Request.Builder()
+                    .url(URL.stripFragment(mUrl))
+                    .head()
+                    .build();
+            mostRecentCall = client.newCall(request);
             try {
-                Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive);
-                changeStatus(STATUS_CHECKING);
-                HttpURLConnection connection;
-                final String hostname = mUrl.getHost();
-                final boolean onion = hostname != null && hostname.endsWith(".onion");
-                if (mUseTor || message.getConversation().getAccount().isOnion() || onion) {
-                    connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
-                } else {
-                    connection = (HttpURLConnection) mUrl.openConnection();
-                }
-                if (method == Method.P1_S3) {
-                    connection.setRequestMethod("GET");
-                    connection.addRequestProperty("Range", "bytes=0-0");
-                } else {
-                    connection.setRequestMethod("HEAD");
-                }
-                connection.setUseCaches(false);
-                Log.d(Config.LOGTAG, "url: " + connection.getURL().toString());
-                connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
-                if (connection instanceof HttpsURLConnection) {
-                    mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
-                }
-                connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
-                connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
-                connection.connect();
-                String contentLength;
-                if (method == Method.P1_S3) {
-                    String contentRange = connection.getHeaderField("Content-Range");
-                    String[] contentRangeParts = contentRange == null ? new String[0] : contentRange.split("/");
-                    if (contentRangeParts.length != 2) {
-                        contentLength = null;
-                    } else {
-                        contentLength = contentRangeParts[1];
-                    }
-                } else {
-                    contentLength = connection.getHeaderField("Content-Length");
-                }
-                final String contentType = connection.getContentType();
-                final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
+                final Response response = mostRecentCall.execute();
+                final String contentLength = response.header("Content-Length");
+                final String contentType = response.header("Content-Type");
+                final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath());
                 if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) {
                     final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
                     if (fileExtension != null) {
@@ -378,8 +329,7 @@ public class HttpDownloadConnection implements Transferable {
                         setupFile();
                     }
                 }
-                connection.disconnect();
-                if (contentLength == null) {
+                if (Strings.isNullOrEmpty(contentLength)) {
                     throw new IOException("no content-length found in HEAD response");
                 }
                 return Long.parseLong(contentLength, 10);
@@ -397,8 +347,6 @@ public class HttpDownloadConnection implements Transferable {
 
         private final boolean interactive;
 
-        private OutputStream os;
-
         public FileDownloader(boolean interactive) {
             this.interactive = interactive;
         }
@@ -411,9 +359,10 @@ public class HttpDownloadConnection implements Transferable {
                 decryptIfNeeded();
                 updateImageBounds();
                 finish();
-            } catch (SSLHandshakeException e) {
+            } catch (final SSLHandshakeException e) {
                 changeStatus(STATUS_OFFER);
-            } catch (Exception e) {
+            } catch (final Exception e) {
+                Log.d(Config.LOGTAG,"problem downloading",e);
                 if (interactive) {
                     showToastForException(e);
                 } else {
@@ -425,67 +374,57 @@ public class HttpDownloadConnection implements Transferable {
         }
 
         private void download() throws Exception {
-            InputStream is = null;
-            HttpURLConnection connection = null;
-            final PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock(Thread.currentThread());
-            try {
-                wakeLock.acquire();
-                if (mUseTor || message.getConversation().getAccount().isOnion()) {
-                    connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
-                } else {
-                    connection = (HttpURLConnection) mUrl.openConnection();
-                }
-                if (connection instanceof HttpsURLConnection) {
-                    mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
-                }
-                connection.setUseCaches(false);
-                connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
-                final long expected = file.getExpectedSize();
-                final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
-                long resumeSize = 0;
-
-                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);

src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java 🔗

@@ -1,250 +1,194 @@
 package eu.siacs.conversations.http;
 
-import android.os.PowerManager;
 import android.util.Log;
 
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
+import 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);
+    }
+}

src/main/java/eu/siacs/conversations/http/Method.java 🔗

@@ -33,7 +33,7 @@ import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.xmpp.XmppConnection;
 
 public enum  Method {
-	P1_S3, HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
+	HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
 
 	public static Method determine(Account account) {
 		XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
@@ -44,8 +44,6 @@ public enum  Method {
 			return HTTP_UPLOAD_LEGACY;
 		} else if (features.httpUpload(0)) {
 			return HTTP_UPLOAD;
-		} else if (features.p1S3FileTransfer()) {
-			return P1_S3;
 		} else {
 			return HTTP_UPLOAD;
 		}

src/main/java/eu/siacs/conversations/http/P1S3UrlStreamHandler.java 🔗

@@ -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;
-		}
-	}
-}

src/main/java/eu/siacs/conversations/http/SlotRequester.java 🔗

@@ -31,9 +31,9 @@ package eu.siacs.conversations.http;
 
 import android.util.Log;
 
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.HashMap;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
@@ -44,147 +44,114 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import okhttp3.Headers;
+import okhttp3.HttpUrl;
 
 public class SlotRequester {
 
-	private final XmppConnectionService service;
-
-	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);
+        }
+    }
 }

src/main/java/eu/siacs/conversations/http/URL.java 🔗

@@ -0,0 +1,34 @@
+package eu.siacs.conversations.http;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+
+import eu.siacs.conversations.http.AesGcmURL;
+import okhttp3.HttpUrl;
+
+public class URL {
+
+    public static final List<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();
+    }
+
+}

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -33,7 +33,6 @@ import eu.siacs.conversations.entities.ReadByMarker;
 import eu.siacs.conversations.entities.ReceiptRequest;
 import eu.siacs.conversations.entities.RtpSessionStatus;
 import eu.siacs.conversations.http.HttpConnectionManager;
-import eu.siacs.conversations.http.P1S3UrlStreamHandler;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.services.XmppConnectionService;
@@ -408,8 +407,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
         final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
         final Element oob = packet.findChild("x", Namespace.OOB);
-        final Element xP1S3 = packet.findChild("x", Namespace.P1_S3_FILE_TRANSFER);
-        final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
         final String oobUrl = oob != null ? oob.findChildContent("url") : null;
         final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
         final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
@@ -464,7 +461,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             }
         }
 
-        if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
+        if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) {
             final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
             final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
             final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
@@ -504,13 +501,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                 }
             }
             final Message message;
-            if (xP1S3url != null) {
-                message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
-                message.setOob(true);
-                if (CryptoHelper.isPgpEncryptedUrl(xP1S3url.toString())) {
-                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
-                }
-            } else if (pgpEncrypted != null && Config.supportOpenPgp()) {
+            if (pgpEncrypted != null && Config.supportOpenPgp()) {
                 message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
             } else if (axolotlEncrypted != null && Config.supportOmemo()) {
                 Jid origin;

src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -416,9 +416,9 @@ public class FileBackend {
         }
     }
 
-    public static void updateFileParams(Message message, URL url, long size) {
+    public static void updateFileParams(Message message, String url, long size) {
         final StringBuilder body = new StringBuilder();
-        body.append(url.toString()).append('|').append(size);
+        body.append(url).append('|').append(size);
         message.setBody(body.toString());
     }
 
@@ -1305,7 +1305,7 @@ public class FileBackend {
         updateFileParams(message, null);
     }
 
-    public void updateFileParams(Message message, URL url) {
+    public void updateFileParams(Message message, String url) {
         DownloadableFile file = getFile(message);
         final String mime = file.getMimeType();
         final boolean privateMessage = message.isPrivateMessage();
@@ -1315,7 +1315,7 @@ public class FileBackend {
         final boolean pdf = "application/pdf".equals(mime);
         final StringBuilder body = new StringBuilder();
         if (url != null) {
-            body.append(url.toString());
+            body.append(url);
         }
         body.append('|').append(file.getSize());
         if (image || video || (pdf && Compatibility.runsTwentyOne())) {

src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java 🔗

@@ -1,6 +1,7 @@
 package eu.siacs.conversations.services;
 
 import android.content.Context;
+import android.os.FileUtils;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.util.Log;
@@ -13,8 +14,10 @@ import org.bouncycastle.crypto.modes.GCMBlockCipher;
 import org.bouncycastle.crypto.params.AEADParameters;
 import org.bouncycastle.crypto.params.KeyParameter;
 
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.security.InvalidAlgorithmParameterException;
@@ -23,12 +26,21 @@ import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
 import java.util.concurrent.atomic.AtomicLong;
 
+import javax.annotation.Nullable;
 import javax.crypto.NoSuchPaddingException;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.utils.Compatibility;
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.ForwardingSink;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
 
 import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
 
@@ -42,7 +54,7 @@ public class AbstractConnectionManager {
         this.mXmppConnectionService = service;
     }
 
-    public static InputStream upgrade(DownloadableFile file, InputStream is) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, NoSuchProviderException {
+    public static InputStream upgrade(DownloadableFile file, InputStream is) {
         if (file.getKey() != null && file.getIv() != null) {
             AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
             cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
@@ -52,6 +64,43 @@ public class AbstractConnectionManager {
         }
     }
 
+
+    //For progress tracking see:
+    //https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
+
+    public static RequestBody requestBody(final DownloadableFile file, final ProgressListener progressListener) {
+        return new RequestBody() {
+
+            @Override
+            public long contentLength() {
+                return file.getSize() + (file.getKey() != null ? 16 : 0);
+            }
+
+            @Nullable
+            @Override
+            public MediaType contentType() {
+                return MediaType.parse(file.getMimeType());
+            }
+
+            @Override
+            public void writeTo(final BufferedSink sink) throws IOException {
+                long transmitted = 0;
+                try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) {
+                    long read;
+                    while ((read = source.read(sink.buffer(), 8196)) != -1) {
+                        transmitted += read;
+                        sink.flush();
+                        progressListener.onProgress(transmitted);
+                    }
+                }
+            }
+        };
+    }
+
+    public interface ProgressListener {
+        void onProgress(long progress);
+    }
+
     public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) {
         FileOutputStream os;
         try {
@@ -121,6 +170,7 @@ public class AbstractConnectionManager {
         }
 
         public static Extension of(String path) {
+            //TODO accept List<String> pathSegments
             final int pos = path.lastIndexOf('/');
             final String filename = path.substring(pos + 1).toLowerCase();
             final String[] parts = filename.split("\\.");

src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java 🔗

@@ -51,13 +51,8 @@ public class ChannelDiscoveryService {
 
     void initializeMuclumbusService() {
         final OkHttpClient.Builder builder = new OkHttpClient.Builder();
-
         if (service.useTorToConnect()) {
-            try {
-                builder.proxy(HttpConnectionManager.getProxy());
-            } catch (IOException e) {
-                throw new RuntimeException("Unable to use Tor proxy", e);
-            }
+            builder.proxy(HttpConnectionManager.getProxy());
         }
         Retrofit retrofit = new Retrofit.Builder()
                 .client(builder.build())

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -104,7 +104,6 @@ import eu.siacs.conversations.generator.AbstractGenerator;
 import eu.siacs.conversations.generator.IqGenerator;
 import eu.siacs.conversations.generator.MessageGenerator;
 import eu.siacs.conversations.generator.PresenceGenerator;
-import eu.siacs.conversations.http.CustomURLStreamHandlerFactory;
 import eu.siacs.conversations.http.HttpConnectionManager;
 import eu.siacs.conversations.parser.AbstractParser;
 import eu.siacs.conversations.parser.IqParser;
@@ -183,10 +182,6 @@ public class XmppConnectionService extends Service {
 
     private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
 
-    static {
-        URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory());
-    }
-
     public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
     private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
     private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression");

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -1605,7 +1605,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
     }
 
     private void createNewConnection(final Message message) {
-        if (!activity.xmppConnectionService.getHttpConnectionManager().checkConnection(message)) {
+        if (!activity.xmppConnectionService.hasInternetConnection()) {
             Toast.makeText(getActivity(), R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
             return;
         }

src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java 🔗

@@ -1071,9 +1071,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
                 } else {
                     this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
                 }
-            } else if (features.p1S3FileTransfer()) {
-                this.binding.serverInfoHttpUploadDescription.setText(R.string.p1_s3_filetransfer);
-                this.binding.serverInfoHttpUpload.setText(R.string.server_info_available);
             } else {
                 this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable);
             }

src/main/java/eu/siacs/conversations/ui/LocationActivity.java 🔗

@@ -98,11 +98,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
 		config.load(ctx, getPreferences());
 		config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE);
 		if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
-			try {
-				config.setHttpProxy(HttpConnectionManager.getProxy());
-			} catch (IOException e) {
-				throw new RuntimeException("Unable to configure proxy");
-			}
+			config.setHttpProxy(HttpConnectionManager.getProxy());
 		}
 	}
 

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -6,6 +6,7 @@ import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.graphics.Typeface;
+import android.net.Uri;
 import android.preference.PreferenceManager;
 import android.text.Spannable;
 import android.text.SpannableString;
@@ -31,6 +32,7 @@ import androidx.core.content.ContextCompat;
 
 import com.google.common.base.Strings;
 
+import java.net.URI;
 import java.net.URL;
 import java.util.List;
 import java.util.Locale;
@@ -48,7 +50,6 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.Message.FileParams;
 import eu.siacs.conversations.entities.RtpSessionStatus;
 import eu.siacs.conversations.entities.Transferable;
-import eu.siacs.conversations.http.P1S3UrlStreamHandler;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.NotificationService;
@@ -800,21 +801,13 @@ public class MessageAdapter extends ArrayAdapter<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);
     }

src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java 🔗

@@ -94,10 +94,10 @@ public class ShareUtil {
 			url = message.getBody();
 		} else if (message.hasFileOnRemoteHost()) {
 			resId = R.string.file_url;
-			url = message.getFileParams().url.toString();
+			url = message.getFileParams().url;
 		} else {
 			final Message.FileParams fileParams = message.getFileParams();
-			url = (fileParams != null && fileParams.url != null) ? fileParams.url.toString() : message.getBody().trim();
+			url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim();
 			resId = R.string.file_url;
 		}
 		if (activity.copyTextToClipboard(url, resId)) {

src/main/java/eu/siacs/conversations/utils/CryptoHelper.java 🔗

@@ -31,7 +31,6 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.http.AesGcmURLStreamHandler;
 import eu.siacs.conversations.xmpp.Jid;
 
 public final class CryptoHelper {
@@ -278,28 +277,6 @@ public final class CryptoHelper {
         }
     }
 
-    public static URL toAesGcmUrl(URL url) {
-        if (!url.getProtocol().equalsIgnoreCase("https")) {
-            return url;
-        }
-        try {
-            return new URL(AesGcmURLStreamHandler.PROTOCOL_NAME + url.toString().substring(url.getProtocol().length()));
-        } catch (MalformedURLException e) {
-            return url;
-        }
-    }
-
-    public static URL toHttpsUrl(URL url) {
-        if (!url.getProtocol().equalsIgnoreCase(AesGcmURLStreamHandler.PROTOCOL_NAME)) {
-            return url;
-        }
-        try {
-            return new URL("https" + url.toString().substring(url.getProtocol().length()));
-        } catch (MalformedURLException e) {
-            return url;
-        }
-    }
-
     public static boolean isPgpEncryptedUrl(String url) {
         if (url == null) {
             return false;

src/main/java/eu/siacs/conversations/utils/MessageUtils.java 🔗

@@ -29,88 +29,94 @@
 
 package eu.siacs.conversations.utils;
 
+import android.net.Uri;
+
 import com.google.common.base.Strings;
 
-import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.util.regex.Pattern;
 
 import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.http.AesGcmURLStreamHandler;
-import eu.siacs.conversations.http.P1S3UrlStreamHandler;
+import eu.siacs.conversations.http.AesGcmURL;
+import eu.siacs.conversations.http.URL;
 
 public class MessageUtils {
 
-	private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}");
+    private static final Pattern LTR_RTL = Pattern.compile("(\\u200E[^\\u200F]*\\u200F){3,}");
 
-	private static final String EMPTY_STRING = "";
+    private static final String EMPTY_STRING = "";
 
-	public static String prepareQuote(Message message) {
-		final StringBuilder builder = new StringBuilder();
-		final String body;
-		if (message.hasMeCommand()) {
-			final String nick;
-			if (message.getStatus() == Message.STATUS_RECEIVED) {
-				if (message.getConversation().getMode() == Conversational.MODE_MULTI) {
-					nick = Strings.nullToEmpty(message.getCounterpart().getResource());
-				} else {
-					nick = message.getContact().getPublicDisplayName();
-				}
-			} else {
-				nick =  UIHelper.getMessageDisplayName(message);
-			}
-			body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length());
-		} else {
-			body = message.getMergedBody().toString();
+    public static String prepareQuote(Message message) {
+        final StringBuilder builder = new StringBuilder();
+        final String body;
+        if (message.hasMeCommand()) {
+            final String nick;
+            if (message.getStatus() == Message.STATUS_RECEIVED) {
+                if (message.getConversation().getMode() == Conversational.MODE_MULTI) {
+                    nick = Strings.nullToEmpty(message.getCounterpart().getResource());
+                } else {
+                    nick = message.getContact().getPublicDisplayName();
+                }
+            } else {
+                nick = UIHelper.getMessageDisplayName(message);
+            }
+            body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length());
+        } else {
+            body = message.getMergedBody().toString();
+        }
+        for (String line : body.split("\n")) {
+            if (line.length() <= 0) {
+                continue;
+            }
+            final char c = line.charAt(0);
+            if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0)
+                    || (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) {
+                continue;
+            }
+            if (builder.length() != 0) {
+                builder.append('\n');
+            }
+            builder.append(line.trim());
         }
-		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;
+    }
 }

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -23,7 +23,6 @@ public final class Namespace {
     public static final String NICK = "http://jabber.org/protocol/nick";
     public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline";
     public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind";
-    public static final String P1_S3_FILE_TRANSFER = "p1:s3filetransfer";
     public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0";
     public static final String BOOKMARKS = "storage:bookmarks";
     public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0";

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -1921,10 +1921,6 @@ public class XmppConnection implements Runnable {
             this.blockListRequested = value;
         }
 
-        public boolean p1S3FileTransfer() {
-            return hasDiscoFeature(account.getDomain(), Namespace.P1_S3_FILE_TRANSFER);
-        }
-
         public boolean httpUpload(long filesize) {
             if (Config.DISABLE_HTTP_UPLOAD) {
                 return false;