diff options
author | steckbrief <steckbrief@chefmail.de> | 2016-05-29 20:33:36 +0200 |
---|---|---|
committer | steckbrief <steckbrief@chefmail.de> | 2016-05-29 20:33:36 +0200 |
commit | b1ab7347b92329512bebe57f6624cae33c27036f (patch) | |
tree | 9234c39535e6320d0ca217b9e28e3dfcd0eb56ea | |
parent | d3a2fe5796e18493a8ff641308d2c6d21bfd06a4 (diff) |
FileTransfer reworked (first steps - functionality as is), HttpUpload
separated, some bugfixes
- HttpUpload moved into own package
- FileTransfer managed by a central manager class, several
FileTransferService implementation can be used
- Security initializations moved to ConversationsPlusApplication
- Access to PowerManager moved to ConversationsPlusApplication
- Removed unused code fragments
- Access to HttpConnectionManager is now static
39 files changed, 1165 insertions, 199 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java index a9f0c6ad..26854205 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java @@ -3,12 +3,20 @@ package de.thedevstack.conversationsplus; import android.app.Application; import android.content.Context; import android.content.pm.PackageManager; +import android.os.PowerManager; import android.preference.PreferenceManager; import java.io.File; +import java.security.SecureRandom; +import de.duenndns.ssl.MemorizingTrustManager; +import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.persistance.FileBackend; +import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager; +import de.thedevstack.conversationsplus.services.filetransfer.httpupload.HttpUploadFileTransferService; +import de.thedevstack.conversationsplus.services.filetransfer.jingle.JingleFileTransferService; import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.PRNGFixes; import de.thedevstack.conversationsplus.utils.SerialSingleThreadExecutor; /** @@ -23,6 +31,9 @@ public class ConversationsPlusApplication extends Application { private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor(); private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor(); + private MemorizingTrustManager memorizingTrustManager; + private SecureRandom secureRandom; + /** * Initializes the application and saves its instance. */ @@ -30,8 +41,20 @@ public class ConversationsPlusApplication extends Application { super.onCreate(); ConversationsPlusApplication.instance = this; ConversationsPlusPreferences.init(PreferenceManager.getDefaultSharedPreferences(getAppContext())); + this.initializeSecurity(); ImageUtil.initBitmapCache(); FileBackend.init(); + FileTransferManager.init(new HttpUploadFileTransferService(), new JingleFileTransferService()); + HttpConnectionManager.init(); + } + + /** + * Initializes security features. + */ + private void initializeSecurity() { + PRNGFixes.apply(); + this.secureRandom = new SecureRandom(); + ConversationsPlusApplication.updateMemorizingTrustmanager(); } /** @@ -100,4 +123,31 @@ public class ConversationsPlusApplication extends Application { public static String getNameAndVersion() { return getName() + " " + getVersion(); } + + public static PowerManager.WakeLock createPartialWakeLock(String name) { + PowerManager powerManager = (PowerManager) getAppContext().getSystemService(Context.POWER_SERVICE); + return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); + } + + public static MemorizingTrustManager getMemorizingTrustManager() { + return getInstance().memorizingTrustManager; + } + + public void setMemorizingTrustManager(MemorizingTrustManager trustManager) { + this.memorizingTrustManager = trustManager; + } + + public static void updateMemorizingTrustmanager() { + final MemorizingTrustManager tm; + if (ConversationsPlusPreferences.dontTrustSystemCAs()) { + tm = new MemorizingTrustManager(getAppContext(), null); + } else { + tm = new MemorizingTrustManager(getAppContext()); + } + getInstance().setMemorizingTrustManager(tm); + } + + public static SecureRandom getSecureRandom() { + return getInstance().secureRandom; + } } diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java index bba52954..d0b30d6d 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java @@ -65,12 +65,11 @@ public class PgpEngine { && message.getUuid().equals(uuid)) { message.setBody(os.toString()); message.setEncryption(Message.ENCRYPTION_DECRYPTED); - final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER && (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink()) && ConversationsPlusPreferences.autoAcceptFileSize() > 0) { - manager.createNewDownloadConnection(message); + HttpConnectionManager.createNewDownloadConnection(message); } mXmppConnectionService.updateMessage(message); callback.success(message); diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java index 4bc27a7d..5e302011 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java @@ -34,6 +34,7 @@ import java.util.Random; import java.util.Set; import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Contact; import de.thedevstack.conversationsplus.entities.Conversation; @@ -462,7 +463,7 @@ public class AxolotlServiceImpl implements OnAdvancedStreamFeaturesLoaded, Axolo PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initSign(x509PrivateKey,mXmppConnectionService.getRNG()); + verifier.initSign(x509PrivateKey, ConversationsPlusApplication.getSecureRandom()); verifier.update(axolotlPublicKey.serialize()); byte[] signature = verifier.sign(); IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); @@ -669,7 +670,7 @@ public class AxolotlServiceImpl implements OnAdvancedStreamFeaturesLoaded, Axolo verifier.update(identityKey.serialize()); if (verifier.verify(verification.second)) { try { - mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); + ConversationsPlusApplication.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); String fingerprint = session.getFingerprint(); Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint); setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509); diff --git a/src/main/java/de/thedevstack/conversationsplus/generator/IqGenerator.java b/src/main/java/de/thedevstack/conversationsplus/generator/IqGenerator.java index 9df0cc21..2d4c8f6a 100644 --- a/src/main/java/de/thedevstack/conversationsplus/generator/IqGenerator.java +++ b/src/main/java/de/thedevstack/conversationsplus/generator/IqGenerator.java @@ -20,7 +20,6 @@ import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Conversation; -import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.services.MessageArchiveService; import de.thedevstack.conversationsplus.utils.Xmlns; import de.thedevstack.conversationsplus.xml.Element; @@ -244,18 +243,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(host); - Element request = packet.addChild("request", Xmlns.HTTP_UPLOAD); - request.addChild("filename").setContent(file.getName()); - request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); - if (mime != null) { - request.addChild("content-type").setContent(mime); - } - return packet; - } - public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) { final IqPacket register = new IqPacket(IqPacket.TYPE.SET); diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java index 59c54662..529d12c4 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java @@ -17,6 +17,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.services.AbstractConnectionManager; import de.thedevstack.conversationsplus.services.XmppConnectionService; @@ -24,29 +25,23 @@ import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.utils.SSLSocketHelper; public class HttpConnectionManager extends AbstractConnectionManager { + private static HttpConnectionManager INSTANCE; - public HttpConnectionManager(XmppConnectionService service) { - super(service); - } + public static void init() { + INSTANCE = new HttpConnectionManager(); + } private List<HttpDownloadConnection> downloadConnections = new CopyOnWriteArrayList<>(); private List<HttpUploadConnection> uploadConnections = new CopyOnWriteArrayList<>(); - public HttpDownloadConnection createNewDownloadConnection(Message message) { - return this.createNewDownloadConnection(message, false); + public static HttpDownloadConnection createNewDownloadConnection(Message message) { + return createNewDownloadConnection(message, false); } - public HttpDownloadConnection createNewDownloadConnection(Message message, boolean interactive) { - HttpDownloadConnection connection = new HttpDownloadConnection(this); + public static HttpDownloadConnection createNewDownloadConnection(Message message, boolean interactive) { + HttpDownloadConnection connection = new HttpDownloadConnection(INSTANCE); connection.init(message,interactive); - this.downloadConnections.add(connection); - return connection; - } - - public HttpUploadConnection createNewUploadConnection(Message message, boolean delay) { - HttpUploadConnection connection = new HttpUploadConnection(this); - connection.init(message,delay); - this.uploadConnections.add(connection); + INSTANCE.downloadConnections.add(connection); return connection; } @@ -58,26 +53,24 @@ public class HttpConnectionManager extends AbstractConnectionManager { this.uploadConnections.remove(httpUploadConnection); } - public void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) { + public static void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) { final X509TrustManager trustManager; final HostnameVerifier hostnameVerifier; if (interactive) { - trustManager = mXmppConnectionService.getMemorizingTrustManager(); - hostnameVerifier = mXmppConnectionService - .getMemorizingTrustManager().wrapHostnameVerifier( + trustManager = ConversationsPlusApplication.getMemorizingTrustManager(); + hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager().wrapHostnameVerifier( new StrictHostnameVerifier()); } else { - trustManager = mXmppConnectionService.getMemorizingTrustManager() + trustManager = ConversationsPlusApplication.getMemorizingTrustManager() .getNonInteractive(); - hostnameVerifier = mXmppConnectionService - .getMemorizingTrustManager() + hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager() .wrapHostnameVerifierNonInteractive( new StrictHostnameVerifier()); } try { final SSLContext sc = SSLSocketHelper.getSSLContext(); sc.init(null, new X509TrustManager[]{trustManager}, - mXmppConnectionService.getRNG()); + ConversationsPlusApplication.getSecureRandom()); final SSLSocketFactory sf = sc.getSocketFactory(); final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java index 424dc96a..3facc14a 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java @@ -32,6 +32,7 @@ import de.thedevstack.conversationsplus.services.AbstractConnectionManager; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.utils.FileUtils; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; public class HttpDownloadConnection implements Transferable { @@ -48,7 +49,7 @@ public class HttpDownloadConnection implements Transferable { public HttpDownloadConnection(HttpConnectionManager manager) { this.mHttpConnectionManager = manager; - this.mXmppConnectionService = manager.getXmppConnectionService(); + this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService; } @Override @@ -265,7 +266,7 @@ public class HttpDownloadConnection implements Transferable { private void download() throws SSLHandshakeException, IOException { InputStream is = null; - PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid()); + PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_download_"+message.getUuid()); try { wakeLock.acquire(); HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java index 231a6ca7..7c791e70 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java @@ -30,11 +30,21 @@ import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.ui.UiCallback; import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.utils.Xmlns; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.exceptions.MissingRequiredContentException; +import de.thedevstack.conversationsplus.xmpp.exceptions.MissingRequiredElementException; +import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; +import de.thedevstack.conversationsplus.xmpp.httpupload.HttpUpload; +import de.thedevstack.conversationsplus.xmpp.httpupload.HttpUploadRequestSlotPacketGenerator; +import de.thedevstack.conversationsplus.xmpp.httpupload.HttpUploadSlot; +import de.thedevstack.conversationsplus.xmpp.httpupload.SlotPacketParser; +import de.thedevstack.conversationsplus.xmpp.httpupload.SlotRequestPacket; import de.thedevstack.conversationsplus.xmpp.jid.Jid; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +@Deprecated public class HttpUploadConnection implements Transferable { private HttpConnectionManager mHttpConnectionManager; @@ -57,7 +67,7 @@ public class HttpUploadConnection implements Transferable { public HttpUploadConnection(HttpConnectionManager httpConnectionManager) { this.mHttpConnectionManager = httpConnectionManager; - this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService(); + this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService; } @Override @@ -107,7 +117,7 @@ public class HttpUploadConnection implements Transferable { || message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_OTR) { this.key = new byte[48]; - mXmppConnectionService.getRNG().nextBytes(this.key); + ConversationsPlusApplication.getSecureRandom().nextBytes(this.key); this.file.setKeyAndIv(this.key); } Pair<InputStream,Integer> pair; @@ -119,29 +129,25 @@ public class HttpUploadConnection implements Transferable { } this.file.setExpectedSize(pair.second); this.mFileInputStream = pair.first; - Jid host = account.getXmppConnection().findDiscoItemByFeature(Xmlns.HTTP_UPLOAD); - IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file,mime); + Jid host = account.getXmppConnection().findDiscoItemByFeature(HttpUpload.NAMESPACE); + IqPacket request = HttpUploadRequestSlotPacketGenerator.generate(host, file.getName(), file.getSize(), mime); mXmppConnectionService.sendIqPacket(account, request, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element slot = packet.findChild("slot",Xmlns.HTTP_UPLOAD); - if (slot != null) { - try { - mGetUrl = new URL(slot.findChildContent("get")); - mPutUrl = new URL(slot.findChildContent("put")); - if (!canceled) { - new Thread(new FileUploader()).start(); - } - } catch (MalformedURLException e) { - fail(); - } - } else { - fail(); - } - } else { - fail(); - } + try { + HttpUploadSlot slot = SlotPacketParser.parseGetAndPutUrl(packet); + mGetUrl = new URL(slot.getGetUrl()); + mPutUrl = new URL(slot.getPutUrl()); + if (!canceled) { + new Thread(new FileUploader()).start(); + } + } catch (XmppException e) { + Logging.e("httpupload", e.getMessage()); + fail(); + } catch (MalformedURLException e) { + Logging.e("httpupload", "malformed url retrieved from slot", e); + fail(); + } } }); message.setTransferable(this); @@ -159,7 +165,7 @@ public class HttpUploadConnection implements Transferable { OutputStream os = null; InputStream errorStream = null; HttpURLConnection connection = null; - PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_upload_"+message.getUuid()); + PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_upload_"+message.getUuid()); try { wakeLock.acquire(); Logging.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()); diff --git a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java index a8e5929b..2f1701df 100644 --- a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java @@ -472,7 +472,7 @@ public class MessageParser extends AbstractParser implements && message.treatAsDownloadable() != Message.Decision.NEVER && ConversationsPlusPreferences.autoAcceptFileSize() > 0 && (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())) { - this.mXmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message); + HttpConnectionManager.createNewDownloadConnection(message); } else { if (query == null) { mXmppConnectionService.getNotificationService().push(message); 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..d6ce4769 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java @@ -0,0 +1,31 @@ +package de.thedevstack.conversationsplus.services; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.Transferable; + +/** + * + */ +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); +} 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..02bd04b9 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java @@ -0,0 +1,31 @@ +package de.thedevstack.conversationsplus.services.filetransfer; + +import java.util.ArrayList; +import java.util.List; + +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.FileTransferService; + +/** + * + */ +public abstract class AbstractFileTransferService implements FileTransferService, FileTransferStatusListener { + protected List<Message> failedMessages = new ArrayList<>(); + + @Override + public void onFailure(Message message, boolean delay) { + this.failedMessages.add(message); + } + + @Override + public void onSuccess(Message message, boolean delay) { + if (this.failedMessages.contains(message)) { + this.failedMessages.remove(message); + } + } + + @Override + public boolean accept(Message message) { + return !this.failedMessages.contains(message); + } +} 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..a44cf49d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java @@ -0,0 +1,108 @@ +package de.thedevstack.conversationsplus.services.filetransfer; + +import java.io.InputStream; + +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.MessageUtil; + +/** + * + */ +public class FileTransferEntity implements Transferable { + private final Message message; + private int tries = 0; + private boolean transferred = false; + private boolean canceled = false; + private boolean failed = false; + private long transmitted = 0; + private DownloadableFile file; + private InputStream fileInputStream; + + public FileTransferEntity(Message message) { + this.message = message; + this.message.setTransferable(this); + this.file = FileBackend.getFile(message, false); + } + + @Override + public boolean equals(Object o) { + FileTransferEntity other = (FileTransferEntity)o; + if ((this.message == null && other.message != null) + || (this.message != null && other.message == null)) { + return false; + } else if (this.message == null && other.message == null) { + return true; + } + return this.message.getUuid().equals(other.message.getUuid()); + } + + @Override + public boolean start() { + return false; + } + + @Override + public int getStatus() { + int status = (failed) ? STATUS_FAILED : STATUS_UPLOADING; + return status; + } + + @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.canceled = true; + } + + public void fail() { + this.failed = true; + this.getMessage().setTransferable(null); + MessageUtil.markMessage(this.getMessage(), Message.STATUS_SEND_FAILED); + } + + public Message getMessage() { + return message; + } + + public boolean isCanceled() { + return this.canceled; + } + + public boolean isFailed() { + return failed; + } + + public void updateProgress(long progress) { + this.transmitted += progress; + } + + public DownloadableFile getFile() { + return file; + } + + public void transferred() { + this.transferred = true; + } + + public void setFileInputStream(InputStream fileInputStream) { + this.fileInputStream = fileInputStream; + } + + public InputStream getFileInputStream() { + return fileInputStream; + } +} 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..f7b3f4e2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java @@ -0,0 +1,127 @@ +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; + +/** + * + */ +public class FileTransferManager implements FileTransferService { + private SortedSet<WeightedTransferService> transferServices; + private static final FileTransferManager INSTANCE = new FileTransferManager(); + + 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) { + 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 + */ + @Override + 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 = wts.fileTransferService.transferFile(message, delay); + 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 + */ + @Override + public boolean accept(Message message) { + return message.needsUploading(); + } + + 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/FileTransferStatusListener.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java new file mode 100644 index 00000000..89af5b39 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java @@ -0,0 +1,11 @@ +package de.thedevstack.conversationsplus.services.filetransfer; + +import de.thedevstack.conversationsplus.entities.Message; + +/** + * + */ +public interface FileTransferStatusListener { + void onFailure(Message message, boolean delay); + void onSuccess(Message message, boolean delay); +} 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..8efd498b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java @@ -0,0 +1,52 @@ +package de.thedevstack.conversationsplus.services.filetransfer.httpupload; + +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.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; + } +} 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..6352c7a7 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java @@ -0,0 +1,120 @@ +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.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.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; + InputStream errorStream = 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(Config.LOGTAG, "uploading 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 = this.entity.getFile().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(Config.LOGTAG, "finished uploading file"); + this.entity.transferred(); + URL getUrl = new URL(this.entity.getGetUrl()); + if (this.entity.getKey() != null) { + getUrl = new URL(getUrl.toString() + "#" + CryptoHelper.bytesToHex(this.entity.getKey())); + } + Message message = this.entity.getMessage(); + MessageUtil.updateFileParams(message, getUrl); + FileBackend.updateMediaScanner(file, XmppConnectionServiceAccessor.xmppConnectionService); + message.setTransferable(null); + 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 { + errorStream = connection.getErrorStream(); + Logging.e("httpupload", "file upload failed: http code (" + code + ") " + new Scanner(errorStream).useDelimiter("\\A").next()); + this.entity.fail(); + } + } catch (IOException e) { + errorStream = (null != connection) ? connection.getErrorStream() : null; + String httpResponseMessage = null; + if (null != errorStream) { + httpResponseMessage = new Scanner(errorStream).useDelimiter("\\A").next(); + } + Logging.e("httpupload", ((null != httpResponseMessage) ? ("http response: " + httpResponseMessage + ", ") : "") + "exception message: " + e.getMessage()); + this.entity.fail(); + } finally { + StreamUtil.close(os); + StreamUtil.close(errorStream); + StreamUtil.close(fileInputStream); + if (connection != null) { + connection.disconnect(); + } + wakeLock.release(); + } + } +}
\ 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..59957d1e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java @@ -0,0 +1,84 @@ +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) { + boolean started = false; + try { + final HttpFileTransferEntity entity = new HttpFileTransferEntity(message, delay); + 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); + 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); + 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..e0b2332a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java @@ -0,0 +1,34 @@ +package de.thedevstack.conversationsplus.services.filetransfer.httpupload; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.entities.Account; +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(); + } + } +} 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..25a16d78 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java @@ -0,0 +1,33 @@ +package de.thedevstack.conversationsplus.services.filetransfer.httpupload; + +import android.app.PendingIntent; + +import de.thedevstack.conversationsplus.entities.Message; +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(); + } + + @Override + public void userInputRequried(PendingIntent pi, Message object) { + this.entity.fail(); + } +} 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..46ee7ce5 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java @@ -0,0 +1,51 @@ +package de.thedevstack.conversationsplus.services.filetransfer.jingle; + +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.services.FileTransferService; +import de.thedevstack.conversationsplus.xmpp.jingle.JingleConnection; +import de.thedevstack.conversationsplus.xmpp.jingle.JingleConnectionManager; + +/** + * + */ +public class JingleFileTransferService 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 + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java index de69c1e6..99d26b64 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java @@ -45,6 +45,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.ui.dialogs.UserDecisionDialog; import de.thedevstack.conversationsplus.ui.listeners.ResizePictureUserDecisionListener; import de.thedevstack.conversationsplus.utils.ConversationUtil; @@ -620,7 +621,7 @@ public class ConversationActivity extends XmppActivity Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); } } else if (message.treatAsDownloadable() != Message.Decision.NEVER) { - xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true); + HttpConnectionManager.createNewDownloadConnection(message, true); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java index f065a25b..d6e6f34c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java @@ -46,6 +46,7 @@ import java.util.Collections; import java.util.List; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.http.HttpDownloadConnection; import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog; import de.thedevstack.conversationsplus.Config; @@ -685,8 +686,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } private void downloadFile(Message message) { - activity.xmppConnectionService.getHttpConnectionManager() - .createNewDownloadConnection(message,true); + HttpConnectionManager.createNewDownloadConnection(message,true); } private void cancelTransmission(Message message) { diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/SettingsActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/SettingsActivity.java index 84eb7016..296f8f4f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/SettingsActivity.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/SettingsActivity.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Locale; import de.duenndns.ssl.MemorizingTrustManager; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.services.ExportLogsService; import de.tzur.conversations.Settings; import de.thedevstack.conversationsplus.R; @@ -71,7 +72,7 @@ public class SettingsActivity extends XmppActivity implements removeCertsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - final MemorizingTrustManager mtm = xmppConnectionService.getMemorizingTrustManager(); + final MemorizingTrustManager mtm = ConversationsPlusApplication.getMemorizingTrustManager(); final ArrayList<String> aliases = Collections.list(mtm.getCertificates()); if (aliases.size() == 0) { displayToast(getString(R.string.toast_no_trusted_certs)); @@ -178,7 +179,7 @@ public class SettingsActivity extends XmppActivity implements xmppConnectionService.refreshAllPresences(); } } else if (name.equals("dont_trust_system_cas")) { - xmppConnectionService.updateMemorizingTrustmanager(); + ConversationsPlusApplication.updateMemorizingTrustmanager(); reconnectAccounts(); } else if ("parse_emoticons".equals(name)) { EmojiconHandler.setParseEmoticons(Settings.PARSE_EMOTICONS); diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/AbstractIqPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/AbstractIqPacketParser.java new file mode 100644 index 00000000..d777cb64 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/AbstractIqPacketParser.java @@ -0,0 +1,43 @@ +package de.thedevstack.conversationsplus.xmpp; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.exceptions.MissingRequiredContentException; +import de.thedevstack.conversationsplus.xmpp.exceptions.MissingRequiredElementException; + +/** + * + */ +public abstract class AbstractIqPacketParser { + protected static Element findRequiredChild(Element element, String elementName, String namespace) throws MissingRequiredElementException { + if (null == element) { + return null; + } + Element child = element.findChild(elementName, namespace); + if (child == null) { + throw new MissingRequiredElementException(elementName, namespace, element); + } + return child; + } + + protected static String findRequiredChildContent(Element element, String elementName) throws MissingRequiredContentException { + if (null == element) { + return null; + } + String childContent = element.findChildContent(elementName); + if (null == childContent) { + throw new MissingRequiredContentException(elementName, element); + } + return childContent; + } + + protected static String findRequiredChildContent(Element element, String elementName, String namespace) throws MissingRequiredContentException { + if (null == element) { + return null; + } + String childContent = element.findChildContent(elementName, namespace); + if (null == childContent) { + throw new MissingRequiredContentException(elementName, namespace, element); + } + return childContent; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java index a89f308f..c5a8fe3b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java @@ -2,7 +2,6 @@ package de.thedevstack.conversationsplus.xmpp; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.security.KeyChain; @@ -50,6 +49,7 @@ import javax.net.ssl.X509TrustManager; 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.dto.SrvRecord; import de.thedevstack.conversationsplus.Config; @@ -199,8 +199,7 @@ public class XmppConnection implements Runnable { public XmppConnection(final Account account, final XmppConnectionService service) { this.account = account; - this.wakeLock = service.getPowerManager().newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString()); + this.wakeLock = ConversationsPlusApplication.createPartialWakeLock(account.getJid().toBareJid().toString()); tagWriter = new TagWriter(); mXmppConnectionService = service; } @@ -382,14 +381,14 @@ public class XmppConnection implements Runnable { private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException { final SSLContext sc = SSLSocketHelper.getSSLContext(); - MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager(); + MemorizingTrustManager trustManager = ConversationsPlusApplication.getMemorizingTrustManager(); KeyManager[] keyManager; if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) { keyManager = new KeyManager[]{mKeyManager}; } else { keyManager = null; } - sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, mXmppConnectionService.getRNG()); + sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, ConversationsPlusApplication.getSecureRandom()); final SSLSocketFactory factory = sc.getSocketFactory(); final HostnameVerifier verifier; if (mInteractive) { @@ -737,13 +736,13 @@ public class XmppConnection implements Runnable { final Element auth = new Element("auth"); auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) { - saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); + saslMechanism = new External(tagWriter, account, ConversationsPlusApplication.getSecureRandom()); } else if (mechanisms.contains("SCRAM-SHA-1")) { - saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); + saslMechanism = new ScramSha1(tagWriter, account, ConversationsPlusApplication.getSecureRandom()); } else if (mechanisms.contains("PLAIN")) { saslMechanism = new Plain(tagWriter, account); } else if (mechanisms.contains("DIGEST-MD5")) { - saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); + saslMechanism = new DigestMd5(tagWriter, account, ConversationsPlusApplication.getSecureRandom()); } if (saslMechanism != null) { final JSONObject keys = account.getKeys(); @@ -1171,7 +1170,7 @@ public class XmppConnection implements Runnable { } private String nextRandomId() { - return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); + return new BigInteger(50, ConversationsPlusApplication.getSecureRandom()).toString(32); } public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredContentException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredContentException.java new file mode 100644 index 00000000..8b21cb9c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredContentException.java @@ -0,0 +1,26 @@ +package de.thedevstack.conversationsplus.xmpp.exceptions; + +import de.thedevstack.conversationsplus.xml.Element; + +/** + * + */ +public class MissingRequiredContentException extends XmppException { + private String elementName; + private String namespace; + + public MissingRequiredContentException(String elementName, Element context) { + super(context); + this.elementName = elementName; + } + + public MissingRequiredContentException(String elementName, String namespace, Element context) { + this(elementName, context); + this.namespace = namespace; + } + + @Override + public String getMessage() { + return "Missing required element content " + ((namespace != null) ? namespace + ":" : "") + elementName + " in context " + getContext(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredElementException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredElementException.java new file mode 100644 index 00000000..8c8162ef --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredElementException.java @@ -0,0 +1,26 @@ +package de.thedevstack.conversationsplus.xmpp.exceptions; + +import de.thedevstack.conversationsplus.xml.Element; + +/** + * + */ +public class MissingRequiredElementException extends XmppException { + private String elementName; + private String namespace; + + public MissingRequiredElementException(String elementName, Element context) { + super(context); + this.elementName = elementName; + } + + public MissingRequiredElementException(String elementName, String namespace, Element context) { + this(elementName, context); + this.namespace = namespace; + } + + @Override + public String getMessage() { + return "Missing required element " + ((namespace != null) ? namespace + ":" : "") + elementName + " in context " + getContext(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UnexpectedIqPacketTypeException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UnexpectedIqPacketTypeException.java new file mode 100644 index 00000000..333f5dca --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UnexpectedIqPacketTypeException.java @@ -0,0 +1,25 @@ +package de.thedevstack.conversationsplus.xmpp.exceptions; + +import java.util.Arrays; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * + */ +public class UnexpectedIqPacketTypeException extends XmppException { + private final IqPacket.TYPE current; + private final IqPacket.TYPE[] expected; + + public UnexpectedIqPacketTypeException(Element context, IqPacket.TYPE current, IqPacket.TYPE... expected) { + super(context); + this.expected = expected; + this.current = current; + } + + @Override + public String getMessage() { + return "Unexpected IQ packet type '" + this.current + "' retrieved. One of " + Arrays.toString(expected) + " was expected."; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/XmppException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/XmppException.java new file mode 100644 index 00000000..8c692f5b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/XmppException.java @@ -0,0 +1,56 @@ +package de.thedevstack.conversationsplus.xmpp.exceptions; + +import de.thedevstack.conversationsplus.xml.Element; + +/** + * + */ +public class XmppException extends Exception { + private Element context; + /** + * Constructs a new {@code Exception} that includes the current stack trace. + */ + public XmppException() { + } + + /** + * Constructs a new {@code Exception} that includes the current stack trace. + */ + public XmppException(Element context) { + this.context = context; + } + + /** + * Constructs a new {@code Exception} with the current stack trace and the + * specified cause. + * + * @param throwable the cause of this exception. + */ + public XmppException(Throwable throwable) { + super(throwable); + } + + /** + * Constructs a new {@code Exception} with the current stack trace and the + * specified cause. + * + * @param throwable the cause of this exception. + */ + public XmppException(Element context, Throwable throwable) { + super(throwable); + this.context = context; + } + + @Override + public String getMessage() { + if (null != context) { + return "Error in XMPP Element. XML element is: " + this.context.toString(); + } else { + return super.getMessage(); + } + } + + public Element getContext() { + return context; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUpload.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUpload.java new file mode 100644 index 00000000..28cba280 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUpload.java @@ -0,0 +1,9 @@ +package de.thedevstack.conversationsplus.xmpp.httpupload; + +/** + * + */ +public interface HttpUpload { + String NAMESPACE = "urn:xmpp:http:upload"; + String DEFAULT_MIME_TYPE = "application/octet-stream"; +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadRequestSlotPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadRequestSlotPacketGenerator.java new file mode 100644 index 00000000..59851417 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadRequestSlotPacketGenerator.java @@ -0,0 +1,46 @@ +package de.thedevstack.conversationsplus.xmpp.httpupload; + +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * Generates the IQ Packets for requesting a http upload slot + * as defined in XEP-0363. + * @see <a href="http://xmpp.org/extensions/xep-0363.html">http://xmpp.org/extensions/xep-0363.html</a> + */ +public final class HttpUploadRequestSlotPacketGenerator { + /** + * Generates the IqPacket to request a http upload slot. + * The attributes from and id are not set in here - this is added while sending the packet. + * <pre> + * <iq from='romeo@montague.tld/garden' + * id='step_03' + * to='upload.montague.tld' + * type='get'> + * <request xmlns='urn:xmpp:http:upload'> + * <filename>my_juliet.png</filename> + * <size>23456</size> + * <content-type>image/jpeg</content-type> + * </request> + * </iq> + * </pre> + * @param host the jid of the host to request a slot from + * @param filename the filename of the file which will be transferred + * @param filesize the filesize of the file which will be transferred + * @param mime the mime type of the file which will be transferred - <code>optional</code> and therefore nullable + * @return the IqPacket + */ + public static IqPacket generate(Jid host, String filename, long filesize, String mime) { + SlotRequestPacket packet = new SlotRequestPacket(filename, filesize); + packet.setTo(host); + packet.setMime((mime == null) ? HttpUpload.DEFAULT_MIME_TYPE : mime); + return packet; + } + + /** + * Utility class - avoid instantiation + */ + private HttpUploadRequestSlotPacketGenerator() { + // Helper class - avoid instantiation + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadSlot.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadSlot.java new file mode 100644 index 00000000..14fe5103 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadSlot.java @@ -0,0 +1,22 @@ +package de.thedevstack.conversationsplus.xmpp.httpupload; + +/** + * + */ +public class HttpUploadSlot { + private final String getUrl; + private final String putUrl; + + public HttpUploadSlot(String getUrl, String putUrl) { + this.getUrl = getUrl; + this.putUrl = putUrl; + } + + public String getGetUrl() { + return this.getUrl; + } + + public String getPutUrl() { + return this.putUrl; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java new file mode 100644 index 00000000..0630449a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java @@ -0,0 +1,27 @@ +package de.thedevstack.conversationsplus.xmpp.httpupload; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.AbstractIqPacketParser; +import de.thedevstack.conversationsplus.xmpp.exceptions.UnexpectedIqPacketTypeException; +import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * + */ +public final class SlotPacketParser extends AbstractIqPacketParser { + public static HttpUploadSlot parseGetAndPutUrl(IqPacket packet) throws XmppException { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element slot = findRequiredChild(packet, "slot", HttpUpload.NAMESPACE); + + String getUrl = findRequiredChildContent(slot, "get"); + String putUrl = findRequiredChildContent(slot, "put"); + + return new HttpUploadSlot(getUrl, putUrl); + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + throw new XmppException(); // Do proper handling of error cases + } else { + throw new UnexpectedIqPacketTypeException(packet, packet.getType(), IqPacket.TYPE.RESULT, IqPacket.TYPE.ERROR); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotRequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotRequestPacket.java new file mode 100644 index 00000000..d470d2f5 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotRequestPacket.java @@ -0,0 +1,53 @@ +package de.thedevstack.conversationsplus.xmpp.httpupload; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * + */ +public class SlotRequestPacket extends IqPacket { + public static final String ELEMENT_NAME = "request"; + public static final String FILENAME_ELEMENT_NAME = "filename"; + public static final String FILESIZE_ELEMENT_NAME = "size"; + public static final String MIME_ELEMENT_NAME = "content-type"; + private Element requestElement; + private String filename; + private long filesize; + private String mime; + + private SlotRequestPacket() { + super(TYPE.GET); + this.requestElement = super.addChild(SlotRequestPacket.ELEMENT_NAME, HttpUpload.NAMESPACE); + } + + public SlotRequestPacket(String filename, long filesize) { + this(); + this.setFilename(filename); + this.setFilesize(filesize); + } + + public void setFilename(String filename) { + if (null == filename || filename.isEmpty()) { + throw new IllegalArgumentException("filename must not be null or empty."); + } + this.filename = filename; + this.requestElement.addChild(FILENAME_ELEMENT_NAME).setContent(filename); + } + + public void setFilesize(long filesize) { + if (0 >= filesize) { + throw new IllegalArgumentException("filesize must not be null or empty."); + } + this.filesize = filesize; + this.requestElement.addChild(FILESIZE_ELEMENT_NAME).setContent(String.valueOf(filesize)); + } + + public void setMime(String mime) { + if (null == mime || mime.isEmpty()) { + throw new IllegalArgumentException("mime type must not be null or empty."); + } + this.mime = mime; + this.requestElement.addChild(MIME_ELEMENT_NAME).setContent(mime); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java index 422bdff0..ca823926 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java @@ -33,6 +33,7 @@ import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.AbstractConnectionManager; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.utils.FileUtils; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -157,8 +158,7 @@ public class JingleConnection implements Transferable { public JingleConnection(JingleConnectionManager mJingleConnectionManager) { this.mJingleConnectionManager = mJingleConnectionManager; - this.mXmppConnectionService = mJingleConnectionManager - .getXmppConnectionService(); + this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService; } public String getSessionId() { diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java index 2deb79c6..30ed9024 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java @@ -31,10 +31,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { @SuppressLint("TrulyRandom") private SecureRandom random = new SecureRandom(); - public JingleConnectionManager(XmppConnectionService service) { - super(service); - } - public void deliverPacket(Account account, JinglePacket packet) { if (packet.isAction("session-initiate")) { JingleConnection connection = new JingleConnection(this); diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java index 9ea268d5..6480251f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java @@ -14,6 +14,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.utils.StreamUtil; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.entities.DownloadableFile; @@ -84,7 +85,7 @@ public class JingleSocks5Transport extends JingleTransport { @Override public void run() { InputStream fileInputStream = null; - final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_"+connection.getSessionId()); + final PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("jingle_send_"+connection.getSessionId()); try { wakeLock.acquire(); MessageDigest digest = MessageDigest.getInstance("SHA-1"); @@ -134,7 +135,7 @@ public class JingleSocks5Transport extends JingleTransport { @Override public void run() { OutputStream fileOutputStream = null; - final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_"+connection.getSessionId()); + final PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("jingle_receive_"+connection.getSessionId()); try { wakeLock.acquire(); MessageDigest digest = MessageDigest.getInstance("SHA-1"); |