aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/de/thedevstack/conversationsplus/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/thedevstack/conversationsplus/services')
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java17
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java36
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java3
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java118
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java23
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java234
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureReason.java91
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureType.java24
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java177
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusEnum.java23
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java12
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java87
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java152
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java91
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java35
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java34
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java52
17 files changed, 1098 insertions, 111 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java
index 7c937475..5456430a 100644
--- a/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java
+++ b/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java
@@ -31,22 +31,14 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import de.thedevstack.conversationsplus.Config;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.entities.DownloadableFile;
public class AbstractConnectionManager {
- protected XmppConnectionService mXmppConnectionService;
-
- public AbstractConnectionManager(XmppConnectionService service) {
- this.mXmppConnectionService = service;
- }
-
- public XmppConnectionService getXmppConnectionService() {
- return this.mXmppConnectionService;
- }
public boolean hasStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return mXmppConnectionService.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ return ConversationsPlusApplication.getAppContext().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
} else {
return true;
}
@@ -125,9 +117,4 @@ public class AbstractConnectionManager {
return null;
}
}
-
- public PowerManager.WakeLock createWakeLock(String name) {
- PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
- return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name);
- }
}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java
new file mode 100644
index 00000000..9fc35b2b
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java
@@ -0,0 +1,36 @@
+package de.thedevstack.conversationsplus.services;
+
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferStatusListener;
+
+/**
+ * An implementation of this class transfers a file to another entity or server.
+ */
+public interface FileTransferService {
+ /**
+ * Transfers a file for the corresponding message.
+ * @param message the message containing the file to transfer
+ * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise
+ */
+ boolean transferFile(Message message);
+ /**
+ * Transfers a file for the corresponding message.
+ * @param message the message containing the file to transfer
+ * @param delay whether the message is delayed or not
+ * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise
+ */
+ boolean transferFile(Message message, boolean delay);
+
+ /**
+ * Checks whether a message can be sent using this service or not.
+ * @param message the message to be checked
+ * @return <code>true</code> if the message can be processed, <code>false</code> otherwise
+ */
+ boolean accept(Message message);
+
+ /**
+ * Adds one or more file transfer status listeners.
+ * @param listeners the listeners to add
+ */
+ void addFileTransferStatusListener(FileTransferStatusListener... listeners);
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java b/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java
index cd55b42f..6402f4ed 100644
--- a/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java
+++ b/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java
@@ -10,6 +10,7 @@ import java.util.List;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.Config;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.R;
import de.thedevstack.conversationsplus.entities.Account;
import de.thedevstack.conversationsplus.entities.Conversation;
@@ -290,7 +291,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.account = account;
this.start = start;
this.end = end;
- this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
+ this.queryId = new BigInteger(50, ConversationsPlusApplication.getSecureRandom()).toString(32);
}
private Query page(String reference) {
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java
index 9c0e55db..83cefc80 100644
--- a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java
+++ b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java
@@ -12,7 +12,6 @@ import android.graphics.Bitmap;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
-import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -58,8 +57,7 @@ import de.duenndns.ssl.MemorizingTrustManager;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
-import de.thedevstack.conversationsplus.exceptions.FileCopyException;
-import de.thedevstack.conversationsplus.utils.FileUtils;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager;
import de.thedevstack.conversationsplus.utils.ImageUtil;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import de.thedevstack.conversationsplus.utils.UiUpdateHelper;
@@ -97,7 +95,6 @@ import de.thedevstack.conversationsplus.ui.UiCallback;
import de.thedevstack.conversationsplus.utils.CryptoHelper;
import de.thedevstack.conversationsplus.utils.ExceptionHelper;
import de.thedevstack.conversationsplus.utils.OnPhoneContactsLoadedListener;
-import de.thedevstack.conversationsplus.utils.PRNGFixes;
import de.thedevstack.conversationsplus.utils.PhoneHelper;
import de.thedevstack.conversationsplus.utils.Xmlns;
import de.thedevstack.conversationsplus.xml.Element;
@@ -148,7 +145,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
startService(intent);
}
};
- private MemorizingTrustManager mMemorizingTrustManager;
private NotificationService mNotificationService = new NotificationService(
this);
private OnMessagePacketReceived mMessageParser = new MessageParser(this);
@@ -169,8 +165,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private MessageGenerator mMessageGenerator = new MessageGenerator();
private PresenceGenerator mPresenceGenerator = new PresenceGenerator();
private List<Account> accounts;
- private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
- this);
+ private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager();
public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
@Override
@@ -197,8 +192,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
};
- private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
- this);
private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
private PushManagementService mPushManagementService = new PushManagementService(this);
private OnConversationUpdate mOnConversationUpdate = null;
@@ -239,7 +232,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private int mucRosterChangedListenerCount = 0;
private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
private int keyStatusUpdatedListenerCount = 0;
- private SecureRandom mRandom;
private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20);
private final OnBindListener mOnBindListener = new OnBindListener() {
@@ -311,7 +303,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
&& checkListeners();
Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode "+Boolean.toString(pushMode));
if (!disabled && !pushMode) {
- int timeToReconnect = mRandom.nextInt(20) + 10;
+ int timeToReconnect = ConversationsPlusApplication.getSecureRandom().nextInt(20) + 10;
scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
}
} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
@@ -334,8 +326,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private OpenPgpServiceConnection pgpServiceConnection;
private PgpEngine mPgpEngine = null;
private WakeLock wakeLock;
- private PowerManager pm;
- private LruCache<String, Bitmap> mBitmapCache;
private Thread mPhoneContactMergerThread;
private EventReceiver mEventReceiver = new EventReceiver();
@@ -577,17 +567,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
@Override
public void onCreate() {
ExceptionHelper.init(getApplicationContext());
- PRNGFixes.apply();
- this.mRandom = new SecureRandom();
- updateMemorizingTrustmanager();
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
- this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
- @Override
- protected int sizeOf(final String key, final Bitmap bitmap) {
- return bitmap.getByteCount() / 1024;
- }
- };
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
this.accounts = databaseBackend.getAccounts();
@@ -614,8 +595,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
this.pgpServiceConnection.bindToService();
}
- this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService");
+ this.wakeLock = ConversationsPlusApplication.createPartialWakeLock("XmppConnectionService");
toggleForegroundService();
updateUnreadCountBadge();
UiUpdateHelper.initXmppConnectionService(this);
@@ -628,7 +608,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
super.onTrimMemory(level);
if (level >= TRIM_MEMORY_COMPLETE) {
Log.d(Config.LOGTAG, "clear cache due to low memory");
- getBitmapCache().evictAll();
+ ImageUtil.evictBitmapCache();
}
}
@@ -739,16 +719,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
- private void sendFileMessage(final Message message, final boolean delay) {
- Logging.d(Config.LOGTAG, "send file message");
- final Account account = message.getConversation().getAccount();
- if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize())) {
- mHttpConnectionManager.createNewUploadConnection(message, delay);
- } else {
- mJingleConnectionManager.createNewConnection(message);
- }
- }
-
public void sendMessage(final Message message) {
sendMessage(message, false, false);
}
@@ -776,28 +746,23 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
if (account.isOnlineAndConnected()) {
+ FileTransferManager fileTransferManager = FileTransferManager.getInstance();
switch (message.getEncryption()) {
case Message.ENCRYPTION_NONE:
- if (message.needsUploading()) {
- if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize())
- || message.fixCounterpart()) {
- this.sendFileMessage(message, delay);
- } else {
- break;
- }
+ if (fileTransferManager.accept(message)) {
+ if (!fileTransferManager.transferFile(message, delay)) {
+ break;
+ }
} else {
packet = mMessageGenerator.generateChat(message);
}
break;
case Message.ENCRYPTION_PGP:
case Message.ENCRYPTION_DECRYPTED:
- if (message.needsUploading()) {
- if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize())
- || message.fixCounterpart()) {
- this.sendFileMessage(message, delay);
- } else {
- break;
- }
+ if (fileTransferManager.accept(message)) {
+ if (!fileTransferManager.transferFile(message, delay)) {
+ break;
+ }
} else {
packet = mMessageGenerator.generatePgpChat(message);
}
@@ -810,7 +775,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} catch (InvalidJidException e) {
break;
}
- if (message.needsUploading()) {
+ if (message.needsUploading()) { //TODO: Use FileTransferManager with a preselection of filetransfer method
mJingleConnectionManager.createNewConnection(message);
} else {
packet = mMessageGenerator.generateOtrChat(message);
@@ -828,13 +793,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
break;
case Message.ENCRYPTION_AXOLOTL:
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
- if (message.needsUploading()) {
- if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize())
- || message.fixCounterpart()) {
- this.sendFileMessage(message, delay);
- } else {
- break;
- }
+ if (fileTransferManager.accept(message)) {
+ if (!fileTransferManager.transferFile(message, delay)) {
+ break;
+ }
} else {
XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
if (axolotlMessage == null) {
@@ -1331,7 +1293,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
callback.onAccountCreated(account);
if (Config.X509_VERIFICATION) {
try {
- getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+ ConversationsPlusApplication.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
} catch (CertificateException e) {
callback.informUser(R.string.certificate_chain_is_not_trusted);
}
@@ -1359,7 +1321,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
databaseBackend.updateAccount(account);
if (Config.X509_VERIFICATION) {
try {
- getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+ ConversationsPlusApplication.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
} catch (CertificateException e) {
showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
}
@@ -1901,7 +1863,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
return;
}
- String name = new BigInteger(75, getRNG()).toString(32);
+ String name = new BigInteger(75, ConversationsPlusApplication.getSecureRandom()).toString(32);
Jid jid = Jid.fromParts(name, server, null);
final Conversation conversation = findOrCreateConversation(account, jid, true);
joinMuc(conversation, new OnConferenceJoined() {
@@ -2170,7 +2132,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public boolean renewSymmetricKey(Conversation conversation) {
Account account = conversation.getAccount();
byte[] symmetricKey = new byte[32];
- this.mRandom.nextBytes(symmetricKey);
+ ConversationsPlusApplication.getSecureRandom().nextBytes(symmetricKey);
Session otrSession = conversation.getOtrSession();
if (otrSession != null) {
MessagePacket packet = new MessagePacket();
@@ -2440,36 +2402,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
- public SecureRandom getRNG() {
- return this.mRandom;
- }
-
- public MemorizingTrustManager getMemorizingTrustManager() {
- return this.mMemorizingTrustManager;
- }
-
- public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
- this.mMemorizingTrustManager = trustManager;
- }
-
- public void updateMemorizingTrustmanager() {
- final MemorizingTrustManager tm;
- if (ConversationsPlusPreferences.dontTrustSystemCAs()) {
- tm = new MemorizingTrustManager(getApplicationContext(), null);
- } else {
- tm = new MemorizingTrustManager(getApplicationContext());
- }
- setMemorizingTrustManager(tm);
- }
-
- public PowerManager getPowerManager() {
- return this.pm;
- }
-
- public LruCache<String, Bitmap> getBitmapCache() {
- return this.mBitmapCache;
- }
-
public void syncRosterToDisk(final Account account) {
Runnable runnable = new Runnable() {
@@ -2599,10 +2531,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
return this.mNotificationService;
}
- public HttpConnectionManager getHttpConnectionManager() {
- return this.mHttpConnectionManager;
- }
-
public void resendFailedMessages(final Message message) {
if (message.getStatus() == Message.STATUS_SEND_FAILED) {
message.setTime(System.currentTimeMillis());
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java
new file mode 100644
index 00000000..c24603b7
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java
@@ -0,0 +1,23 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.services.FileTransferService;
+
+/**
+ *
+ */
+public abstract class AbstractFileTransferService implements FileTransferService {
+ private List<FileTransferStatusListener> statusListeners = new ArrayList<>();
+
+ @Override
+ public void addFileTransferStatusListener(FileTransferStatusListener... listeners) {
+ this.statusListeners.addAll(Arrays.asList(listeners));
+ }
+
+ protected void addStatusListenerToEntity(FileTransferEntity entity) {
+ entity.addFileTransferStatusListener(this.statusListeners.toArray(new FileTransferStatusListener[0]));
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java
new file mode 100644
index 00000000..e1b40fa6
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java
@@ -0,0 +1,234 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.entities.DownloadableFile;
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.entities.Transferable;
+import de.thedevstack.conversationsplus.persistance.FileBackend;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+
+/**
+ *
+ */
+public class FileTransferEntity implements Transferable {
+ /**
+ * Listeners to inform about a status change.
+ */
+ private final List<FileTransferStatusListener> statusListeners = new ArrayList<>();
+ /**
+ * The associated message.
+ */
+ private final Message message;
+ /**
+ * Number of attempts.
+ */
+ private int attempts = 0;
+ /**
+ * The status of the file transfer.
+ */
+ private FileTransferStatusEnum transferStatus;
+ /**
+ * Number of bytes transmitted.
+ */
+ private long transmitted = 0;
+ /**
+ * The associated file.
+ */
+ private DownloadableFile file;
+ /**
+ * The associated file as stream.
+ */
+ private InputStream fileInputStream;
+
+ /**
+ * Initializes the FileTransferEntity based on the associated message.
+ * This initialization includes loading the file and associating this transferable to the message.
+ * @param message the message in which the file to transfer is contained.
+ */
+ public FileTransferEntity(Message message) {
+ this.message = message;
+ this.message.setTransferable(this);
+ this.file = FileBackend.getFile(message, false);
+ }
+
+ /**
+ * Start something.
+ * Empty implementation since documentation in interface is missing.
+ * @return <code>false</code>
+ */
+ @Override
+ public boolean start() {
+ return false;
+ }
+
+ /**
+ * Returns the global transferable status.
+ *
+ * @return {@value STATUS_FAILED} if #isFailed returns <code>true</code>, {@value STATUS_UPLOADING} otherwise
+ */
+ @Override
+ public int getStatus() {
+ int status = (isFailed()) ? STATUS_FAILED : STATUS_UPLOADING;
+ return status;
+ }
+
+ /**
+ * Returns the expected file size of the underlying file.
+ * @return the expected file size or 0 if no file is associated.
+ */
+ @Override
+ public long getFileSize() {
+ return file == null ? 0 : file.getExpectedSize();
+ }
+
+ /**
+ * Calculates the current progress in percent.
+ *
+ * @return the current progress in percent
+ */
+ @Override
+ public int getProgress() {
+ if (file == null) {
+ return 0;
+ }
+ return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
+ }
+
+ /**
+ * Cancels the file transfer and informs the listeners about cancellation.
+ */
+ @Override
+ public void cancel() {
+ this.transferStatus = FileTransferStatusEnum.CANCELED;
+
+ this.close();
+
+ for (FileTransferStatusListener listener : this.statusListeners) {
+ listener.onCancel(this);
+ }
+ }
+
+ /**
+ * Starts an transfer attempt.
+ */
+ public void startAttempt() {
+ this.attempts++;
+ this.transferStatus = FileTransferStatusEnum.TRANSFERRING;
+ }
+
+ /**
+ * Fails an file transfer and informs the listeners about failure.
+ *
+ * @param failureReason the reason of failure.
+ */
+ public void fail(FileTransferFailureReason failureReason) {
+ this.transferStatus = FileTransferStatusEnum.FAILED;
+
+ this.close();
+
+ failureReason.setAttempt(this.attempts);
+ for (FileTransferStatusListener listener : this.statusListeners) {
+ listener.onFailure(this, failureReason);
+ }
+ }
+
+ /**
+ * Updates the progress by adding parameter value to current progress value.
+ *
+ * @param progress the number of newly transferred bytes
+ */
+ public void updateProgress(long progress) {
+ if (0 == this.attempts) {
+ this.startAttempt();
+ }
+ this.transmitted += progress;
+ }
+
+ /**
+ * Set the status of the file transfer to FileTransferStatusEnum#TRANSFERRED and informs the listeners about success.
+ */
+ public void transferred() {
+ this.transferStatus = FileTransferStatusEnum.TRANSFERRED;
+
+ this.close();
+
+ for (FileTransferStatusListener listener : this.statusListeners) {
+ listener.onSuccess(this);
+ }
+ }
+
+ /**
+ * Closes the file input stream (if it is not yet closed) and removes association with message.
+ */
+ private void close() {
+ StreamUtil.close(this.fileInputStream);
+ this.getMessage().setTransferable(null);
+ }
+
+ /**
+ * Whether the file is transferred or not.
+ * @return <code>true</code> if the file is successfully transferred, <code>false</code> otherwise
+ */
+ public boolean isTransferred() {
+ return FileTransferStatusEnum.TRANSFERRED == this.transferStatus;
+ }
+
+ /**
+ * Whether the file transfer is canceled or not.
+ * @return <code>true</code> if the file transfer was canceled, <code>false</code> otherwise
+ */
+ public boolean isCanceled() {
+ return FileTransferStatusEnum.CANCELED == this.transferStatus;
+ }
+
+ /**
+ * Whether the file transfer failed or not.
+ * @return <code>true</code> if the file transfer failed, <code>false</code> otherwise
+ */
+ public boolean isFailed() {
+ return FileTransferStatusEnum.FAILED == this.transferStatus;
+ }
+
+ public void setFileInputStream(InputStream fileInputStream) {
+ this.fileInputStream = fileInputStream;
+ }
+
+ public InputStream getFileInputStream() {
+ return fileInputStream;
+ }
+
+ public Message getMessage() {
+ return message;
+ }
+
+ public DownloadableFile getFile() {
+ return file;
+ }
+
+ public void addFileTransferStatusListener(FileTransferStatusListener... listeners) {
+ this.statusListeners.addAll(Arrays.asList(listeners));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FileTransferEntity that = (FileTransferEntity) o;
+
+ String uuid = message != null ? message.getUuid() : null;
+ String thatUuid = that.message != null ? that.message.getUuid() : null;
+
+ return uuid != null ? uuid.equals(thatUuid) : thatUuid == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return message != null && message.getUuid() != null ? message.getUuid().hashCode() : 0;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureReason.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureReason.java
new file mode 100644
index 00000000..353c34d8
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureReason.java
@@ -0,0 +1,91 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+/**
+ *
+ */
+public final class FileTransferFailureReason {
+ private final FileTransferFailureType type;
+ private int attempt;
+ private Exception causedByException;
+ private String failureMessage;
+
+ public static FileTransferFailureReason createRecoverableFailureReason(Exception e) {
+ return createFailureReason(FileTransferFailureType.RECOVERABLE, e.getMessage(), e);
+ }
+
+ public static FileTransferFailureReason createRecoverableFailureReason(String reason) {
+ return createFailureReason(FileTransferFailureType.RECOVERABLE, reason, null);
+ }
+
+ public static FileTransferFailureReason createRecoverableFailureReason(Exception e, String failureMessage) {
+ return createFailureReason(FileTransferFailureType.RECOVERABLE, failureMessage, e);
+ }
+
+ public static FileTransferFailureReason createLimitedRecoverableFailureReason(Exception e) {
+ return createFailureReason(FileTransferFailureType.LIMITEDRECOVERABLE, e.getMessage(), e);
+ }
+
+ public static FileTransferFailureReason createLimitedRecoverableFailureReason(String reason) {
+ return createFailureReason(FileTransferFailureType.LIMITEDRECOVERABLE, reason, null);
+ }
+
+ public static FileTransferFailureReason createLimitedRecoverableFailureReason(Exception e, String failureMessage) {
+ return createFailureReason(FileTransferFailureType.LIMITEDRECOVERABLE, failureMessage, e);
+ }
+
+ public static FileTransferFailureReason createNonRecoverableFailureReason(Exception e) {
+ return createFailureReason(FileTransferFailureType.NONRECOVERABLE, e.getMessage(), e);
+ }
+
+ public static FileTransferFailureReason createNonRecoverableFailureReason(String reason) {
+ return createFailureReason(FileTransferFailureType.NONRECOVERABLE, reason, null);
+ }
+
+ public static FileTransferFailureReason createNonRecoverableFailureReason(Exception e, String failureMessage) {
+ return createFailureReason(FileTransferFailureType.NONRECOVERABLE, failureMessage, e);
+ }
+
+ private static FileTransferFailureReason createFailureReason(FileTransferFailureType type, String message, Exception e) {
+ FileTransferFailureReason ftfr = new FileTransferFailureReason(type);
+ ftfr.setCausedByException(e);
+ if (null != e && (null == message || message.isEmpty())) {
+ message = e.getMessage();
+ }
+ ftfr.setFailureMessage(message);
+
+ return ftfr;
+
+ }
+
+ private FileTransferFailureReason(FileTransferFailureType type) {
+ this.type = type;
+ }
+
+ public void setFailureMessage(String failureMessage) {
+ this.failureMessage = failureMessage;
+ }
+
+ public String getFailureMessage() {
+ return failureMessage;
+ }
+
+ public FileTransferFailureType getType() {
+ return type;
+ }
+
+ public void setCausedByException(Exception causedByException) {
+ this.causedByException = causedByException;
+ }
+
+ public Exception getCausedByException() {
+ return causedByException;
+ }
+
+ public void setAttempt(int attempt) {
+ this.attempt = attempt;
+ }
+
+ public boolean isRecoverable() {
+ return this.type.isRetryPossible(this.attempt);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureType.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureType.java
new file mode 100644
index 00000000..3daca38e
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureType.java
@@ -0,0 +1,24 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+/**
+ *
+ */
+public enum FileTransferFailureType {
+ RECOVERABLE(Integer.MAX_VALUE),
+ LIMITEDRECOVERABLE(5),
+ NONRECOVERABLE(1);
+
+ int maxAttempts;
+
+ FileTransferFailureType(int maxAttempts) {
+ this.maxAttempts = maxAttempts;
+ }
+
+ public boolean isRetryPossible(int attempt) {
+ return attempt < this.maxAttempts;
+ }
+
+ public int getMaxAttempts() {
+ return this.maxAttempts;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java
new file mode 100644
index 00000000..017b88ea
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java
@@ -0,0 +1,177 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.Config;
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.services.FileTransferService;
+import de.thedevstack.conversationsplus.services.filetransfer.httpupload.HttpFileTransferEntity;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+
+/**
+ *
+ */
+public class FileTransferManager implements FileTransferStatusListener {
+ private SortedSet<WeightedTransferService> transferServices;
+ private static final FileTransferManager INSTANCE = new FileTransferManager();
+ private final HashMap<String, WeightedTransferService> activeTransfers = new HashMap<>();
+
+ private FileTransferManager() {
+ this.transferServices = new TreeSet<>();
+ }
+
+ public static FileTransferManager getInstance() {
+ return INSTANCE;
+ }
+
+ public static void init(FileTransferService... fileTransferServices) {
+ if (null != fileTransferServices && fileTransferServices.length > 0) {
+ for (FileTransferService fts : fileTransferServices) {
+ addFileTransferService(fts);
+ }
+ }
+ }
+
+ public static void init(HashMap<Integer, FileTransferService> fileTransferServices) {
+ for (Map.Entry<Integer, FileTransferService> entry : fileTransferServices.entrySet()) {
+ addFileTransferService(entry.getKey(), entry.getValue());
+ }
+ }
+
+ public static void addFileTransferService(int weight, FileTransferService fts) {
+ fts.addFileTransferStatusListener(INSTANCE);
+ INSTANCE.transferServices.add(new WeightedTransferService(weight, fts));
+ }
+
+ public static void addFileTransferService(FileTransferService fts) {
+ int weight = 1;
+ if (!INSTANCE.transferServices.isEmpty()) {
+ weight = INSTANCE.transferServices.last().weight + 1;
+ }
+ addFileTransferService(weight, fts);
+ }
+
+ /**
+ * Transfers a file for the corresponding message.
+ *
+ * @param message the message containing the file to transfer
+ * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise
+ */
+ public boolean transferFile(Message message) {
+ return this.transferFile(message, false);
+ }
+
+ /**
+ * Transfers a file for the corresponding message.
+ *
+ * @param message the message containing the file to transfer
+ * @param delay whether the message is delayed or not
+ * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise
+ */
+ public boolean transferFile(Message message, boolean delay) {
+ Logging.d(Config.LOGTAG, "send file message");
+ boolean transferSuccessfullyStarted = false;
+ for (WeightedTransferService wts : this.transferServices) {
+ try {
+ if (wts.fileTransferService.accept(message)) {
+ transferSuccessfullyStarted = this.startFileTransfer(message, delay, wts);
+ if (transferSuccessfullyStarted) {
+ break;
+ }
+ }
+ } catch (Exception e) {
+ //TODO Do real exception handling!!!!!
+ }
+ }
+ return transferSuccessfullyStarted;
+ }
+
+ /**
+ * Checks whether a message can be sent using this service or not.
+ *
+ * @param message the message to be checked
+ * @return <code>true</code> if the message can be processed, <code>false</code> otherwise
+ */
+ public boolean accept(Message message) {
+ return message.needsUploading();
+ }
+
+ @Override
+ public void onFailure(FileTransferEntity entity, FileTransferFailureReason failureReason) {
+ WeightedTransferService wts = this.activeTransfers.get(entity.getMessage().getUuid());
+ if (null == wts) {
+ return;
+ }
+ boolean delayed = (entity instanceof HttpFileTransferEntity) && ((HttpFileTransferEntity) entity).isDelayed();
+ if (failureReason.isRecoverable()) {
+ wts.fileTransferService.transferFile(entity.getMessage(), delayed);
+ } else {
+ boolean retransferStarted = false;
+ this.activeTransfers.remove(entity.getMessage().getUuid());
+ for (WeightedTransferService newWts : this.transferServices.tailSet(wts)) {
+ if (newWts == wts) { // Same Reference
+ continue;
+ }
+ if (newWts.fileTransferService.accept(entity.getMessage())) {
+ retransferStarted = startFileTransfer(entity.getMessage(), delayed, newWts);
+ if (retransferStarted) {
+ break;
+ }
+ }
+ }
+ if (!retransferStarted) {
+ MessageUtil.markMessage(entity.getMessage(), Message.STATUS_SEND_FAILED);
+ }
+ }
+ }
+
+ @Override
+ public void onCancel(FileTransferEntity entity) {
+ this.activeTransfers.remove(entity.getMessage().getUuid());
+ MessageUtil.markMessage(entity.getMessage(), Message.STATUS_SEND_FAILED); // TODO New Status CANCELED!
+ }
+
+ @Override
+ public void onSuccess(FileTransferEntity entity) {
+ this.activeTransfers.remove(entity.getMessage().getUuid());
+ }
+
+ private boolean startFileTransfer(Message message, boolean delayed, WeightedTransferService wts) {
+ boolean transferSuccessfullyStarted = wts.fileTransferService.transferFile(message, delayed);
+ if (transferSuccessfullyStarted) {
+ this.activeTransfers.put(message.getUuid(), wts);
+ }
+ return transferSuccessfullyStarted;
+ }
+
+ static class WeightedTransferService implements Comparable<WeightedTransferService> {
+ int weight;
+ FileTransferService fileTransferService;
+
+ WeightedTransferService(int weight, FileTransferService service) {
+ this.weight = weight;
+ this.fileTransferService = service;
+ }
+
+ /**
+ * Compares this object to the specified object to determine their relative
+ * order.
+ *
+ * @param another the object to compare to this instance.
+ * @return a negative integer if this instance is less than {@code another};
+ * a positive integer if this instance is greater than
+ * {@code another}; 0 if this instance has the same order as
+ * {@code another}.
+ * @throws ClassCastException if {@code another} cannot be converted into something
+ * comparable to {@code this} instance.
+ */
+ @Override
+ public int compareTo(WeightedTransferService another) {
+ return this.weight - another.weight;
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusEnum.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusEnum.java
new file mode 100644
index 00000000..e55d94d8
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusEnum.java
@@ -0,0 +1,23 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+/**
+ * Status values of a file transfer.
+ */
+public enum FileTransferStatusEnum {
+ /**
+ * The file transfer is in progress.
+ */
+ TRANSFERRING,
+ /**
+ * The file transfer was finished successfully.
+ */
+ TRANSFERRED,
+ /**
+ * The file transfer failed.
+ */
+ FAILED,
+ /**
+ * The file transfer was canceled.
+ */
+ CANCELED;
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java
new file mode 100644
index 00000000..86d6fbfa
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java
@@ -0,0 +1,12 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+import de.thedevstack.conversationsplus.entities.Message;
+
+/**
+ *
+ */
+public interface FileTransferStatusListener {
+ void onFailure(FileTransferEntity entity, FileTransferFailureReason failureReason);
+ void onSuccess(FileTransferEntity entity);
+ void onCancel(FileTransferEntity entity);
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java
new file mode 100644
index 00000000..a8e5734f
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java
@@ -0,0 +1,87 @@
+package de.thedevstack.conversationsplus.services.filetransfer.httpupload;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.Config;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferEntity;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason;
+import de.thedevstack.conversationsplus.utils.CryptoHelper;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.xmpp.httpupload.HttpUploadSlot;
+
+/**
+ *
+ */
+public class HttpFileTransferEntity extends FileTransferEntity {
+ private HttpUploadSlot slot;
+ private final byte[] key;
+ private final boolean delayed;
+
+ public HttpFileTransferEntity(Message message, boolean delayed) {
+ super(message);
+ this.getMessage().setHttpUploaded(true);
+ this.getMessage().setNoDownloadable();
+ if (Config.ENCRYPT_ON_HTTP_UPLOADED
+ || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
+ || message.getEncryption() == Message.ENCRYPTION_OTR) {
+ this.key = new byte[48];
+ ConversationsPlusApplication.getSecureRandom().nextBytes(this.key);
+ this.getFile().setKeyAndIv(this.key);
+ } else {
+ this.key = null;
+ }
+ this.delayed = delayed;
+ }
+
+ public void setSlot(HttpUploadSlot slot) {
+ this.slot = slot;
+ }
+
+ public String getGetUrl() {
+ return this.slot.getGetUrl();
+ }
+
+ public String getPutUrl() {
+ return this.slot.getPutUrl();
+ }
+
+ public byte[] getKey() {
+ return key;
+ }
+
+ public boolean isDelayed() {
+ return this.delayed;
+ }
+
+ @Override
+ public void fail(FileTransferFailureReason failureReason) {
+ this.getMessage().setHttpUploaded(false);
+ super.fail(failureReason);
+ }
+
+ @Override
+ public void cancel() {
+ this.getMessage().setHttpUploaded(false);
+ super.cancel();
+ }
+
+ @Override
+ public void transferred() {
+ try {
+ URL getUrl = new URL(this.getGetUrl());
+ if (this.getKey() != null) {
+ getUrl = new URL(getUrl.toString() + "#" + CryptoHelper.bytesToHex(this.getKey()));
+ }
+
+ MessageUtil.updateFileParams(this.getMessage(), getUrl);
+ } catch (MalformedURLException e) {
+ Logging.e("httpupload", "Not a valid get url");
+ }
+
+ super.transferred();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java
new file mode 100644
index 00000000..8edc5be7
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java
@@ -0,0 +1,152 @@
+package de.thedevstack.conversationsplus.services.filetransfer.httpupload;
+
+import android.os.PowerManager;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.Scanner;
+
+import javax.net.ssl.HttpsURLConnection;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.Config;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.entities.DownloadableFile;
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.http.HttpConnectionManager;
+import de.thedevstack.conversationsplus.persistance.FileBackend;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason;
+import de.thedevstack.conversationsplus.utils.CryptoHelper;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import de.thedevstack.conversationsplus.utils.UiUpdateHelper;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+import de.thedevstack.conversationsplus.xmpp.httpupload.HttpUpload;
+
+public class HttpFileUploader implements Runnable {
+ private static final String HTTP_METHOD = "PUT";
+ private static final String MIME_REQUEST_PROPERTY_NAME = "Content-Type";
+ private static final String USER_AGENT_REQUEST_PROPERTY_NAME = "User-Agent";
+ private static final int BUFFER_LENGTH = 4096;
+ private final HttpFileTransferEntity entity;
+
+ public HttpFileUploader(HttpFileTransferEntity entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public void run() {
+ this.upload();
+ }
+
+ private void upload() {
+ OutputStream os = null;
+
+ HttpURLConnection connection = null;
+ InputStream fileInputStream = null;
+ DownloadableFile file = this.entity.getFile();
+ PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_upload_" + entity.getMessage().getUuid());
+ try {
+ wakeLock.acquire();
+ Logging.d("httpupload", "uploading file to " + this.entity.getPutUrl());
+ URL putUrl = new URL(this.entity.getPutUrl());
+ fileInputStream = this.entity.getFileInputStream();
+ connection = (HttpURLConnection) putUrl.openConnection();
+
+ if (connection instanceof HttpsURLConnection) {
+ HttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true);
+ }
+ connection.setRequestMethod(HTTP_METHOD);
+ connection.setFixedLengthStreamingMode((int) file.getExpectedSize());
+ String mime = file.getMimeType();
+ connection.setRequestProperty(MIME_REQUEST_PROPERTY_NAME, mime == null ? HttpUpload.DEFAULT_MIME_TYPE : mime);
+ connection.setRequestProperty(USER_AGENT_REQUEST_PROPERTY_NAME, ConversationsPlusApplication.getNameAndVersion());
+ connection.setDoOutput(true);
+ connection.connect();
+ os = connection.getOutputStream();
+ int count = -1;
+ byte[] buffer = new byte[BUFFER_LENGTH];
+ while (((count = fileInputStream.read(buffer)) != -1) && !this.entity.isCanceled()) {
+ this.entity.updateProgress(count);
+ os.write(buffer, 0, count);
+ UiUpdateHelper.updateConversationUi();
+ }
+ os.flush();
+ os.close();
+ fileInputStream.close();
+ int code = connection.getResponseCode();
+ if (code == 200 || code == 201) {
+ Logging.d("httpupload", "finished uploading file");
+ this.entity.transferred();
+
+ FileBackend.updateMediaScanner(file, XmppConnectionServiceAccessor.xmppConnectionService); // Why???
+
+ // Send the URL to the counterpart
+ Message message = this.entity.getMessage();
+ message.setCounterpart(message.getConversation().getJid().toBareJid());
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ XmppConnectionServiceAccessor.xmppConnectionService.getPgpEngine().encrypt(message, new HttpUploadedFileEncryptionUiCallback(this.entity));
+ } else {
+ XmppConnectionServiceAccessor.xmppConnectionService.resendMessage(message, this.entity.isDelayed());
+ }
+ } else {
+ String httpResponseMessage = this.getErrorStreamContent(connection);
+ String errorMessage = "file upload failed: http code (" + code + ") " + httpResponseMessage;
+ Logging.e("httpupload", errorMessage);
+ FileTransferFailureReason failureReason = null;
+ switch (code) {
+ case 403:
+ failureReason = FileTransferFailureReason.createLimitedRecoverableFailureReason(errorMessage);
+ break;
+ case 404:
+ failureReason = FileTransferFailureReason.createNonRecoverableFailureReason("Upload URL not found");
+ break;
+ case 500:
+ failureReason = FileTransferFailureReason.createNonRecoverableFailureReason("Internal Server Error");
+ break;
+ default:
+ failureReason = FileTransferFailureReason.createRecoverableFailureReason(errorMessage);
+ }
+ this.entity.fail(failureReason);
+ }
+ } catch (UnknownHostException e) {
+ Logging.e("httpupload", "File upload failed due to unknown host. " + e.getMessage());
+ //if (!HAS_INTERNET_CONNECTION) {
+ this.entity.fail(FileTransferFailureReason.createRecoverableFailureReason(e, "Missing internet connection"));
+ //}
+ } catch (IOException e) {
+ String httpResponseMessage = this.getErrorStreamContent(connection);
+ Logging.e("httpupload", ((null != httpResponseMessage) ? ("http response: " + httpResponseMessage + ", ") : "") + "exception message: " + e.getMessage());
+ // TODO: Differentiate IOException while internet connection wasn't available and others
+ this.entity.fail(FileTransferFailureReason.createRecoverableFailureReason(e, httpResponseMessage));
+ } finally {
+ StreamUtil.close(os);
+ StreamUtil.close(fileInputStream);
+ if (connection != null) {
+ connection.disconnect();
+ }
+ wakeLock.release();
+ }
+ }
+
+ private String getErrorStreamContent(HttpURLConnection connection) {
+ InputStream errorStream = null;
+ String httpResponseMessage = null;
+ try {
+ errorStream = connection.getErrorStream();
+ if (null != errorStream) {
+ Scanner scanner = new Scanner(errorStream).useDelimiter("\\A");
+ if (scanner.hasNext()) {
+ httpResponseMessage = scanner.next();
+ }
+ }
+ } finally {
+ StreamUtil.close(errorStream);
+ }
+ return httpResponseMessage;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java
new file mode 100644
index 00000000..fb150a92
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java
@@ -0,0 +1,91 @@
+package de.thedevstack.conversationsplus.services.filetransfer.httpupload;
+
+import android.util.Pair;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.entities.Account;
+import de.thedevstack.conversationsplus.entities.DownloadableFile;
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.persistance.FileBackend;
+import de.thedevstack.conversationsplus.services.AbstractConnectionManager;
+import de.thedevstack.conversationsplus.services.FileTransferService;
+import de.thedevstack.conversationsplus.services.filetransfer.AbstractFileTransferService;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.XmppSendUtil;
+import de.thedevstack.conversationsplus.xmpp.httpupload.HttpUpload;
+import de.thedevstack.conversationsplus.xmpp.httpupload.HttpUploadRequestSlotPacketGenerator;
+import de.thedevstack.conversationsplus.xmpp.jid.Jid;
+import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket;
+
+/**
+ *
+ */
+public class HttpUploadFileTransferService extends AbstractFileTransferService implements FileTransferService {
+
+ public HttpUploadFileTransferService() {
+ }
+
+ /**
+ * Transfers a file for the corresponding message.
+ *
+ * @param message the message containing the file to transfer
+ * @return <code>true</code> if the file transfer was started successfully, <code>false</code> otherwise
+ */
+ @Override
+ public boolean transferFile(Message message) {
+ return this.transferFile(message, false);
+ }
+
+ /**
+ * Transfers a file for the corresponding message.
+ *
+ * @param message the message containing the file to transfer
+ * @param delay whether the message is delayed or not
+ * @return <code>true</code> if the file transfer was started successfully, <code>false</code> otherwise
+ */
+ @Override
+ public boolean transferFile(Message message, boolean delay) {
+ Logging.d("httpupload", "Starting to upload file");
+ boolean started = false;
+ try {
+ final HttpFileTransferEntity entity = new HttpFileTransferEntity(message, delay);
+ this.addStatusListenerToEntity(entity);
+ entity.startAttempt();
+ Account account = message.getConversation().getAccount();
+ DownloadableFile file = entity.getFile();
+ Pair<InputStream, Integer> inputStreamAndExpectedSize = AbstractConnectionManager.createInputStream(file, true);
+
+ entity.setFileInputStream(inputStreamAndExpectedSize.first);
+ file.setExpectedSize(inputStreamAndExpectedSize.second);
+
+ Logging.d("httpupload", "Requesting upload slot for file upload");
+ Jid host = account.getXmppConnection().findDiscoItemByFeature(HttpUpload.NAMESPACE);
+ IqPacket request = HttpUploadRequestSlotPacketGenerator.generate(host, file.getName(), file.getSize(), file.getMimeType());
+ XmppSendUtil.sendIqPacket(account, request, new HttpUploadSlotRequestReceived(entity));
+ MessageUtil.markMessage(message, Message.STATUS_UNSEND);
+
+ Logging.d("httpupload", "Upload slot for file upload requested");
+ started = true;
+ } catch (FileNotFoundException e) {
+ Logging.e("httpupload", "Could not find file, exception message: " + e.getMessage());
+ }
+ return started;
+ }
+
+ /**
+ * Checks whether a message can be sent using this service or not.
+ *
+ * @param message the message to be checked
+ * @return <code>true</code> if the message can be processed, <code>false</code> otherwise
+ */
+ @Override
+ public boolean accept(Message message) {
+ return null != message
+ && null != message.getConversation()
+ && null != message.getConversation().getAccount()
+ && message.getConversation().getAccount().httpUploadAvailable(FileBackend.getFile(message, false).getSize());
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java
new file mode 100644
index 00000000..462a370c
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java
@@ -0,0 +1,35 @@
+package de.thedevstack.conversationsplus.services.filetransfer.httpupload;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.entities.Account;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason;
+import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived;
+import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException;
+import de.thedevstack.conversationsplus.xmpp.httpupload.HttpUploadSlot;
+import de.thedevstack.conversationsplus.xmpp.httpupload.SlotPacketParser;
+import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket;
+
+/**
+ *
+ */
+public class HttpUploadSlotRequestReceived implements OnIqPacketReceived {
+ private final HttpFileTransferEntity entity;
+
+ public HttpUploadSlotRequestReceived(HttpFileTransferEntity entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ try {
+ HttpUploadSlot slot = SlotPacketParser.parseGetAndPutUrl(packet);
+ this.entity.setSlot(slot);
+ if (!this.entity.isCanceled()) {
+ new Thread(new HttpFileUploader(this.entity)).start();
+ }
+ } catch (XmppException e) {
+ Logging.e("httpupload", e.getMessage());
+ this.entity.fail(FileTransferFailureReason.createNonRecoverableFailureReason(e));
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java
new file mode 100644
index 00000000..e3935252
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java
@@ -0,0 +1,34 @@
+package de.thedevstack.conversationsplus.services.filetransfer.httpupload;
+
+import android.app.PendingIntent;
+
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason;
+import de.thedevstack.conversationsplus.ui.UiCallback;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
+/**
+ *
+ */
+public class HttpUploadedFileEncryptionUiCallback implements UiCallback<Message> {
+ private final HttpFileTransferEntity entity;
+
+ public HttpUploadedFileEncryptionUiCallback(HttpFileTransferEntity entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public void success(Message message) {
+ XmppConnectionServiceAccessor.xmppConnectionService.resendMessage(message, this.entity.isDelayed());
+ }
+
+ @Override
+ public void error(int errorCode, Message object) {
+ this.entity.fail(FileTransferFailureReason.createLimitedRecoverableFailureReason("Failed to encrypt"));
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message object) {
+ this.entity.fail(FileTransferFailureReason.createLimitedRecoverableFailureReason("Failed to encrypt, user input would have been required"));
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java
new file mode 100644
index 00000000..5d8ddd4e
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java
@@ -0,0 +1,52 @@
+package de.thedevstack.conversationsplus.services.filetransfer.jingle;
+
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.services.FileTransferService;
+import de.thedevstack.conversationsplus.services.filetransfer.AbstractFileTransferService;
+import de.thedevstack.conversationsplus.xmpp.jingle.JingleConnection;
+import de.thedevstack.conversationsplus.xmpp.jingle.JingleConnectionManager;
+
+/**
+ *
+ */
+public class JingleFileTransferService extends AbstractFileTransferService implements FileTransferService {
+ private final JingleConnectionManager jingleConnectionManager;
+
+ public JingleFileTransferService() {
+ this.jingleConnectionManager = new JingleConnectionManager();
+ }
+ /**
+ * Transfers a file for the corresponding message.
+ *
+ * @param message the message containing the file to transfer
+ * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise
+ */
+ @Override
+ public boolean transferFile(Message message) {
+ return this.transferFile(message, false);
+ }
+
+ /**
+ * Transfers a file for the corresponding message.
+ *
+ * @param message the message containing the file to transfer
+ * @param delay whether the message is delayed or not
+ * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise
+ */
+ @Override
+ public boolean transferFile(Message message, boolean delay) {
+ JingleConnection jingleConnection = this.jingleConnectionManager.createNewConnection(message);
+ return null != jingleConnection;
+ }
+
+ /**
+ * Checks whether a message can be sent using this service or not.
+ *
+ * @param message the message to be checked
+ * @return <code>true</code> if the message can be processed, <code>false</code> otherwise
+ */
+ @Override
+ public boolean accept(Message message) {
+ return message.fixCounterpart(); // No clue why - but this seems to be the check for jingle file transfer
+ }
+}