From 56b5eae48072a05da28c35aff5fe719e057b13ad Mon Sep 17 00:00:00 2001 From: steckbrief Date: Thu, 26 May 2016 20:34:05 +0200 Subject: Fixed javadoc of PubSub Element Generation/Parsing --- .../xmpp/pubsub/PubSubPacket.java | 43 +++++++++++++++++++- .../xmpp/pubsub/PubSubPacketGenerator.java | 46 +++++++++++++++++++++- .../xmpp/pubsub/PubSubPacketParser.java | 30 ++++++++++++-- 3 files changed, 114 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java index fea1383c..fe485de0 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java @@ -4,28 +4,69 @@ import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; /** - * Created by tzur on 15.01.2016. + * Representation of a PubSub IQ packet as defined in XEP-0060. + *
One example: + *
+ *   
+ *      
+ *          
+ *      
+ *   
+ * 
+ * @see http://xmpp.org/extensions/xep-0060.html */ public class PubSubPacket extends IqPacket { + /** + * The namespace of pubsub. + */ public static final String NAMESPACE = "http://jabber.org/protocol/pubsub"; + /** + * The name of the root element. + */ public static final String ELEMENT_NAME = "pubsub"; + /** + * The PubSub element - everything which is added to this packet is a child of this element. + */ private Element pubSubElement; + /** + * Instantiate the PubSubPacket for the given type. + * @param type the IqPacket.TYPE + */ public PubSubPacket(IqPacket.TYPE type) { super(type); this.pubSubElement = super.addChild(PubSubPacket.ELEMENT_NAME, PubSubPacket.NAMESPACE); } + /** + * Adds an element to the PubSub element instead of the IqPacket. + * @param child the children to be added + * @return the added children + */ @Override public Element addChild(Element child) { return this.pubSubElement.addChild(child); } + /** + * Adds an element to the PubSub element instead of the IqPacket. + * @param name name of the children to be added + * @return the added children + */ @Override public Element addChild(String name) { return this.pubSubElement.addChild(name); } + /** + * Adds an element to the PubSub element instead of the IqPacket. + * @param name name of the children to be added + * @param xmlns namespace of the children to be added + * @return the added children + */ @Override public Element addChild(String name, String xmlns) { return this.pubSubElement.addChild(name, xmlns); diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java index 1eb44b5e..a93c37aa 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java @@ -4,10 +4,32 @@ import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; /** - * Created by tzur on 15.01.2016. + * Generates the IQ packets for Pubsub Subscription as defined in XEP-0060. + * @see http://xmpp.org/extensions/xep-0060.html */ public final class PubSubPacketGenerator { + /** + * Generates a pubsub publish packet. + * The attributes from and id are not set in here - this is added while sending the packet. + *
+     * 
+     *   
+     *     
+     *       
+     *         ...
+     *       
+     *     
+     *   
+     * 
+     * 
+ * @param nodeName the name of the publish node + * @param item the item element + * @return the generated PubSubPacket + */ public static PubSubPacket generatePubSubPublishPacket(String nodeName, Element item) { final PubSubPacket pubsub = new PubSubPacket(IqPacket.TYPE.SET); final Element publish = pubsub.addChild("publish"); @@ -16,6 +38,25 @@ public final class PubSubPacketGenerator { return pubsub; } + /** + * Generates a pubsub retrieve packet. + * The attributes from and id are not set in here - this is added while sending the packet. + *
+     * 
+     *   
+     *     
+     *       
+     *     
+     *   
+     * 
+     * 
+ * @param nodeName + * @param item + * @return + */ public static PubSubPacket generatePubSubRetrievePacket(String nodeName, Element item) { final PubSubPacket pubsub = new PubSubPacket(IqPacket.TYPE.GET); final Element items = pubsub.addChild("items"); @@ -26,6 +67,9 @@ public final class PubSubPacketGenerator { return pubsub; } + /** + * Utility class - avoid instantiation + */ private PubSubPacketGenerator() { // Avoid instantiation } diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java index 682803c4..0f803b56 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java @@ -4,16 +4,28 @@ import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; /** - * Created by tzur on 15.01.2016. + * Parses the IQ Packets for handling pubsub + * as defined in XEP-0060. + * @see http://xmpp.org/extensions/xep-0060.html */ -public class PubSubPacketParser { +public final class PubSubPacketParser { + /** + * Finds the pubsub element within an IQ packet. + * @param packet the retrieved IQ packet + * @return the {@value PubSubPacket#ELEMENT_NAME} Element or null if the IqPacket is null or the IQ packet does not contain an pubsub element. + */ public static Element findPubSubPacket(IqPacket packet){ if (null == packet) { return null; } - return packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); + return packet.findChild(PubSubPacket.ELEMENT_NAME, PubSubPacket.NAMESPACE); } + /** + * Finds the "items" element within an pubSubPacket element. + * @param pubSubPacket the pubSubPacket element + * @return the items Element or null if the pubSubPacket is null or the pubSubPacket does not contain an items element. + */ public static Element findItemsFromPubSubElement(Element pubSubPacket) { if (null == pubSubPacket) { return null; @@ -21,7 +33,19 @@ public class PubSubPacketParser { return pubSubPacket.findChild("items"); } + /** + * Finds the "items" element within an pubSubPacket element. + * @param packet the IqPacket element + * @return the items Element or null if the IqPacket is null or the IQ packet does not contain an pubsub element with an items element. + */ public static Element findItems(IqPacket packet) { return PubSubPacketParser.findItemsFromPubSubElement(PubSubPacketParser.findPubSubPacket(packet)); } + + /** + * Utility class - avoid instantiation + */ + private PubSubPacketParser() { + // Avoid instantiation + } } -- cgit v1.2.3 From d3a2fe5796e18493a8ff641308d2c6d21bfd06a4 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Thu, 26 May 2016 20:36:14 +0200 Subject: Fixed javadoc of HttpUploadHint --- .../conversationsplus/xmpp/httpuploadim/HttpUploadHint.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java index 3e9d7f2b..13fb5ad1 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java @@ -3,10 +3,20 @@ package de.thedevstack.conversationsplus.xmpp.httpuploadim; import de.thedevstack.conversationsplus.xml.Element; /** - * Created by steckbrief on 17.04.2016. + * Representation of the HttpUploadHint. + *
+ *  
+ * 
*/ public class HttpUploadHint extends Element { + /** + * The namespace of message processing hints as defined in XEP-0334. + * @see http://xmpp.org/extensions/xep-0334.html + */ public static final String NAMESPACE = "urn:xmpp:hints"; + /** + * The element name of the hint for an http upload. + */ public static final String ELEMENT_NAME = "httpupload"; public HttpUploadHint() { -- cgit v1.2.3 From b1ab7347b92329512bebe57f6624cae33c27036f Mon Sep 17 00:00:00 2001 From: steckbrief Date: Sun, 29 May 2016 20:33:36 +0200 Subject: 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 --- .../ConversationsPlusApplication.java | 50 ++++++++ .../conversationsplus/crypto/PgpEngine.java | 3 +- .../crypto/axolotl/AxolotlServiceImpl.java | 5 +- .../conversationsplus/generator/IqGenerator.java | 13 --- .../http/HttpConnectionManager.java | 39 +++---- .../http/HttpDownloadConnection.java | 5 +- .../http/HttpUploadConnection.java | 52 +++++---- .../conversationsplus/parser/MessageParser.java | 2 +- .../services/AbstractConnectionManager.java | 17 +-- .../services/FileTransferService.java | 31 +++++ .../services/MessageArchiveService.java | 3 +- .../services/XmppConnectionService.java | 118 ++++--------------- .../filetransfer/AbstractFileTransferService.java | 31 +++++ .../services/filetransfer/FileTransferEntity.java | 108 ++++++++++++++++++ .../services/filetransfer/FileTransferManager.java | 127 +++++++++++++++++++++ .../filetransfer/FileTransferStatusListener.java | 11 ++ .../httpupload/HttpFileTransferEntity.java | 52 +++++++++ .../filetransfer/httpupload/HttpFileUploader.java | 120 +++++++++++++++++++ .../httpupload/HttpUploadFileTransferService.java | 84 ++++++++++++++ .../httpupload/HttpUploadSlotRequestReceived.java | 34 ++++++ .../HttpUploadedFileEncryptionUiCallback.java | 33 ++++++ .../jingle/JingleFileTransferService.java | 51 +++++++++ .../conversationsplus/ui/ConversationActivity.java | 3 +- .../conversationsplus/ui/ConversationFragment.java | 4 +- .../conversationsplus/ui/SettingsActivity.java | 5 +- .../xmpp/AbstractIqPacketParser.java | 43 +++++++ .../conversationsplus/xmpp/XmppConnection.java | 17 ++- .../MissingRequiredContentException.java | 26 +++++ .../MissingRequiredElementException.java | 26 +++++ .../UnexpectedIqPacketTypeException.java | 25 ++++ .../xmpp/exceptions/XmppException.java | 56 +++++++++ .../xmpp/httpupload/HttpUpload.java | 9 ++ .../HttpUploadRequestSlotPacketGenerator.java | 46 ++++++++ .../xmpp/httpupload/HttpUploadSlot.java | 22 ++++ .../xmpp/httpupload/SlotPacketParser.java | 27 +++++ .../xmpp/httpupload/SlotRequestPacket.java | 53 +++++++++ .../xmpp/jingle/JingleConnection.java | 4 +- .../xmpp/jingle/JingleConnectionManager.java | 4 - .../xmpp/jingle/JingleSocks5Transport.java | 5 +- 39 files changed, 1165 insertions(+), 199 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/AbstractIqPacketParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredContentException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredElementException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UnexpectedIqPacketTypeException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/XmppException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUpload.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadRequestSlotPacketGenerator.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadSlot.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotRequestPacket.java 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 downloadConnections = new CopyOnWriteArrayList<>(); private List 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 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 true if the file transfer was successful, false 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 true if the file transfer was successful, false 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 true if the message can be processed, false 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 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,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 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(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 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 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 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 fileTransferServices) { + for (Map.Entry 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 true if the file transfer was successful, false 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 true if the file transfer was successful, false 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 true if the message can be processed, false otherwise + */ + @Override + public boolean accept(Message message) { + return message.needsUploading(); + } + + static class WeightedTransferService implements Comparable { + 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 true if the file transfer was started successfully, false 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 true if the file transfer was started successfully, false 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 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 true if the message can be processed, false 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 { + 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 true if the file transfer was successful, false 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 true if the file transfer was successful, false 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 true if the message can be processed, false 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 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 http://xmpp.org/extensions/xep-0363.html + */ +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. + *
+     * 
+     *   
+     *     my_juliet.png
+     *     23456
+     *     image/jpeg
+     *   
+     * 
+     * 
+ * @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 - optional 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"); -- cgit v1.2.3 From f45ad10b1baaf09fd4a40d6b63d1cd093623eedc Mon Sep 17 00:00:00 2001 From: steckbrief Date: Mon, 6 Jun 2016 09:05:50 +0200 Subject: Related to FS#131, FS#129, FS#220: - FileTransferFailureReason including types introduced. A failure can be recoverable, non-recoverable or limited recoverable - in case file transfer with the highest weight factor fails, the next file transfer method is used - improved logging - javadoc comments added --- .../services/FileTransferService.java | 11 +- .../filetransfer/AbstractFileTransferService.java | 22 +-- .../services/filetransfer/FileTransferEntity.java | 194 +++++++++++++++++---- .../filetransfer/FileTransferFailureReason.java | 91 ++++++++++ .../filetransfer/FileTransferFailureType.java | 24 +++ .../services/filetransfer/FileTransferManager.java | 58 +++++- .../filetransfer/FileTransferStatusEnum.java | 23 +++ .../filetransfer/FileTransferStatusListener.java | 5 +- .../httpupload/HttpFileTransferEntity.java | 35 ++++ .../filetransfer/httpupload/HttpFileUploader.java | 74 +++++--- .../httpupload/HttpUploadFileTransferService.java | 7 + .../httpupload/HttpUploadSlotRequestReceived.java | 3 +- .../HttpUploadedFileEncryptionUiCallback.java | 5 +- .../jingle/JingleFileTransferService.java | 3 +- 14 files changed, 472 insertions(+), 83 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureReason.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureType.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusEnum.java diff --git a/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java index d6ce4769..9fc35b2b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java @@ -1,11 +1,10 @@ package de.thedevstack.conversationsplus.services; -import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.entities.Transferable; +import de.thedevstack.conversationsplus.services.filetransfer.FileTransferStatusListener; /** - * + * An implementation of this class transfers a file to another entity or server. */ public interface FileTransferService { /** @@ -28,4 +27,10 @@ public interface FileTransferService { * @return true if the message can be processed, false 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/filetransfer/AbstractFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java index 02bd04b9..c24603b7 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java @@ -1,31 +1,23 @@ package de.thedevstack.conversationsplus.services.filetransfer; import java.util.ArrayList; +import java.util.Arrays; 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 failedMessages = new ArrayList<>(); +public abstract class AbstractFileTransferService implements FileTransferService { + private List statusListeners = new ArrayList<>(); @Override - public void onFailure(Message message, boolean delay) { - this.failedMessages.add(message); + public void addFileTransferStatusListener(FileTransferStatusListener... listeners) { + this.statusListeners.addAll(Arrays.asList(listeners)); } - @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); + 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 index a44cf49d..e1b40fa6 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java @@ -1,60 +1,95 @@ 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.MessageUtil; +import de.thedevstack.conversationsplus.utils.StreamUtil; /** * */ public class FileTransferEntity implements Transferable { + /** + * Listeners to inform about a status change. + */ + private final List statusListeners = new ArrayList<>(); + /** + * The associated message. + */ private final Message message; - private int tries = 0; - private boolean transferred = false; - private boolean canceled = false; - private boolean failed = false; + /** + * 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); } - @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()); - } - + /** + * Start something. + * Empty implementation since documentation in interface is missing. + * @return false + */ @Override public boolean start() { return false; } + /** + * Returns the global transferable status. + * + * @return {@value STATUS_FAILED} if #isFailed returns true, {@value STATUS_UPLOADING} otherwise + */ @Override public int getStatus() { - int status = (failed) ? STATUS_FAILED : STATUS_UPLOADING; + 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) { @@ -63,46 +98,137 @@ public class FileTransferEntity implements Transferable { return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); } + /** + * Cancels the file transfer and informs the listeners about cancellation. + */ @Override public void cancel() { - this.canceled = true; + 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); + } } - public void fail() { - this.failed = true; + /** + * 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); - MessageUtil.markMessage(this.getMessage(), Message.STATUS_SEND_FAILED); } - public Message getMessage() { - return message; + /** + * Whether the file is transferred or not. + * @return true if the file is successfully transferred, false otherwise + */ + public boolean isTransferred() { + return FileTransferStatusEnum.TRANSFERRED == this.transferStatus; } + /** + * Whether the file transfer is canceled or not. + * @return true if the file transfer was canceled, false otherwise + */ public boolean isCanceled() { - return this.canceled; + return FileTransferStatusEnum.CANCELED == this.transferStatus; } + /** + * Whether the file transfer failed or not. + * @return true if the file transfer failed, false otherwise + */ public boolean isFailed() { - return failed; + return FileTransferStatusEnum.FAILED == this.transferStatus; } - public void updateProgress(long progress) { - this.transmitted += progress; + 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 transferred() { - this.transferred = true; + public void addFileTransferStatusListener(FileTransferStatusListener... listeners) { + this.statusListeners.addAll(Arrays.asList(listeners)); } - public void setFileInputStream(InputStream fileInputStream) { - this.fileInputStream = fileInputStream; + @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; + } - public InputStream getFileInputStream() { - return fileInputStream; + @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 index f7b3f4e2..017b88ea 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java @@ -9,13 +9,16 @@ 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 FileTransferService { +public class FileTransferManager implements FileTransferStatusListener { private SortedSet transferServices; private static final FileTransferManager INSTANCE = new FileTransferManager(); + private final HashMap activeTransfers = new HashMap<>(); private FileTransferManager() { this.transferServices = new TreeSet<>(); @@ -40,6 +43,7 @@ public class FileTransferManager implements FileTransferService { } public static void addFileTransferService(int weight, FileTransferService fts) { + fts.addFileTransferStatusListener(INSTANCE); INSTANCE.transferServices.add(new WeightedTransferService(weight, fts)); } @@ -68,14 +72,13 @@ public class FileTransferManager implements FileTransferService { * @param delay whether the message is delayed or not * @return true if the file transfer was successful, false 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); + transferSuccessfullyStarted = this.startFileTransfer(message, delay, wts); if (transferSuccessfullyStarted) { break; } @@ -93,11 +96,58 @@ public class FileTransferManager implements FileTransferService { * @param message the message to be checked * @return true if the message can be processed, false otherwise */ - @Override 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 { int weight; FileTransferService fileTransferService; 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 index 89af5b39..86d6fbfa 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java @@ -6,6 +6,7 @@ import de.thedevstack.conversationsplus.entities.Message; * */ public interface FileTransferStatusListener { - void onFailure(Message message, boolean delay); - void onSuccess(Message message, boolean delay); + 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 index 8efd498b..a8e5734f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java @@ -1,9 +1,16 @@ 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; /** @@ -49,4 +56,32 @@ public class HttpFileTransferEntity extends FileTransferEntity { 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 index 6352c7a7..8edc5be7 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java @@ -7,6 +7,7 @@ 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; @@ -18,6 +19,7 @@ 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; @@ -43,14 +45,14 @@ public class HttpFileUploader implements Runnable { 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()); + Logging.d("httpupload", "uploading file to " + this.entity.getPutUrl()); URL putUrl = new URL(this.entity.getPutUrl()); fileInputStream = this.entity.getFileInputStream(); connection = (HttpURLConnection) putUrl.openConnection(); @@ -60,7 +62,7 @@ public class HttpFileUploader implements Runnable { } connection.setRequestMethod(HTTP_METHOD); connection.setFixedLengthStreamingMode((int) file.getExpectedSize()); - String mime = this.entity.getFile().getMimeType(); + 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); @@ -78,16 +80,13 @@ public class HttpFileUploader implements Runnable { fileInputStream.close(); int code = connection.getResponseCode(); if (code == 200 || code == 201) { - Logging.d(Config.LOGTAG, "finished uploading file"); + Logging.d("httpupload", "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())); - } + + FileBackend.updateMediaScanner(file, XmppConnectionServiceAccessor.xmppConnectionService); // Why??? + + // Send the URL to the counterpart 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)); @@ -95,21 +94,37 @@ public class HttpFileUploader implements Runnable { 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(); + 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) { - errorStream = (null != connection) ? connection.getErrorStream() : null; - String httpResponseMessage = null; - if (null != errorStream) { - httpResponseMessage = new Scanner(errorStream).useDelimiter("\\A").next(); - } + String httpResponseMessage = this.getErrorStreamContent(connection); Logging.e("httpupload", ((null != httpResponseMessage) ? ("http response: " + httpResponseMessage + ", ") : "") + "exception message: " + e.getMessage()); - this.entity.fail(); + // TODO: Differentiate IOException while internet connection wasn't available and others + this.entity.fail(FileTransferFailureReason.createRecoverableFailureReason(e, httpResponseMessage)); } finally { StreamUtil.close(os); - StreamUtil.close(errorStream); StreamUtil.close(fileInputStream); if (connection != null) { connection.disconnect(); @@ -117,4 +132,21 @@ public class HttpFileUploader implements Runnable { 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 index 59957d1e..fb150a92 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java @@ -48,19 +48,26 @@ public class HttpUploadFileTransferService extends AbstractFileTransferService i */ @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 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()); 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 index e0b2332a..462a370c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java @@ -2,6 +2,7 @@ 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; @@ -28,7 +29,7 @@ public class HttpUploadSlotRequestReceived implements OnIqPacketReceived { } } catch (XmppException e) { Logging.e("httpupload", e.getMessage()); - this.entity.fail(); + 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 index 25a16d78..e3935252 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java @@ -3,6 +3,7 @@ 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; @@ -23,11 +24,11 @@ public class HttpUploadedFileEncryptionUiCallback implements UiCallback @Override public void error(int errorCode, Message object) { - this.entity.fail(); + this.entity.fail(FileTransferFailureReason.createLimitedRecoverableFailureReason("Failed to encrypt")); } @Override public void userInputRequried(PendingIntent pi, Message object) { - this.entity.fail(); + 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 index 46ee7ce5..5d8ddd4e 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java @@ -2,13 +2,14 @@ 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 implements FileTransferService { +public class JingleFileTransferService extends AbstractFileTransferService implements FileTransferService { private final JingleConnectionManager jingleConnectionManager; public JingleFileTransferService() { -- cgit v1.2.3 From 97100834a5bcb08f2fdf2eb6c580d3ceeb8b6b2f Mon Sep 17 00:00:00 2001 From: steckbrief Date: Sat, 16 Jul 2016 15:11:36 +0200 Subject: Implements FS#227: Store password encrypted in internal database --- build.gradle | 14 ++- .../conversationsplus/entities/Account.java | 8 +- .../persistance/DatabaseBackend.java | 16 ++- .../conversationsplus/utils/SimpleCryptoUtil.java | 110 +++++++++++++++++ .../utils/SimpleCryptoUtilTest.java | 136 +++++++++++++++++++++ 5 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java create mode 100644 src/test/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtilTest.java diff --git a/build.gradle b/build.gradle index de4d418e..f58ac6df 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,6 @@ buildscript { repositories { jcenter() - mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:2.0.0' @@ -21,6 +20,9 @@ repositories { flatDir { dirs 'libs/3rdParty/jcenter', 'libs/3rdParty/maven' } + maven { + url "http://snippets.thedevstack.de/mvn" + } } dependencies { @@ -55,6 +57,10 @@ dependencies { // Android dependencies compile 'com.android.support:support-v13:23.2.0' + + testCompile 'junit:junit:4.12' + // Optional -- Mockito framework + testCompile 'org.mockito:mockito-core:1.10.19' } ext { @@ -90,6 +96,12 @@ android { playstore free } + testOptions { + unitTests.all { + // All the usual Gradle options. + jvmArgs '-XX:MaxPermSize=256m' + } + } if (project.hasProperty('mStoreFile') && project.hasProperty('mStorePassword') && project.hasProperty('mKeyAlias') && diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Account.java b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java index bc956364..2ee76504 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Account.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java @@ -28,6 +28,7 @@ import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceImpl; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceStub; import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.SimpleCryptoUtil; import de.thedevstack.conversationsplus.xmpp.XmppConnection; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -172,6 +173,8 @@ public class Account extends AbstractEntity { private List bookmarks = new CopyOnWriteArrayList<>(); private final Collection blocklist = new CopyOnWriteArraySet<>(); + public static final String PW_SEED = "sadjgdiahsdkhashp3zt98edAFSFIOKZUIUOz23ejj12ezhez2398iehz"; + public Account() { this.uuid = "0"; } @@ -210,9 +213,10 @@ public class Account extends AbstractEntity { cursor.getString(cursor.getColumnIndex(SERVER)), "mobile"); } catch (final InvalidJidException ignored) { } + String password = SimpleCryptoUtil.decrypt(PW_SEED, cursor.getString(cursor.getColumnIndex(PASSWORD))); return new Account(cursor.getString(cursor.getColumnIndex(UUID)), jid, - cursor.getString(cursor.getColumnIndex(PASSWORD)), + password, cursor.getInt(cursor.getColumnIndex(OPTIONS)), cursor.getString(cursor.getColumnIndex(ROSTERVERSION)), cursor.getString(cursor.getColumnIndex(KEYS)), @@ -342,7 +346,7 @@ public class Account extends AbstractEntity { values.put(UUID, uuid); values.put(USERNAME, jid.getLocalpart()); values.put(SERVER, jid.getDomainpart()); - values.put(PASSWORD, password); + values.put(PASSWORD, SimpleCryptoUtil.encrypt(PW_SEED, password)); values.put(OPTIONS, options); values.put(KEYS, this.keys.toString()); values.put(ROSTERVERSION, rosterVersion); diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java index 5a5746d5..6442e909 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java @@ -44,6 +44,7 @@ import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Roster; import de.thedevstack.conversationsplus.entities.ServiceDiscoveryResult; +import de.thedevstack.conversationsplus.utils.SimpleCryptoUtil; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -54,7 +55,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; private static final int DATABASE_VERSION = 25; private static final int C_TO_CPLUS_VERSION_OFFSET = 1000; - private static final int CPLUS_DATABASE_VERSION = 1; + private static final int CPLUS_DATABASE_VERSION = 2; private static final int CPLUS_DATABASE_VERSION_MULTIPLIER = 100; private static final int PHYSICAL_DATABASE_VERSION = DATABASE_VERSION + C_TO_CPLUS_VERSION_OFFSET + (CPLUS_DATABASE_VERSION * CPLUS_DATABASE_VERSION_MULTIPLIER); @@ -203,6 +204,19 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("INSERT INTO " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + "(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") " + " SELECT " + Message.UUID + " FROM " + Message.TABLENAME); } + if (newVersion == 2) { + Logging.d("db.upgrade.cplus", "Encrypt all passwords for the first time"); + Cursor cursor = db.rawQuery("SELECT " + Account.UUID + ", " + Account.PASSWORD + " FROM " + Account.TABLENAME, new String[0]); + while (cursor.moveToNext()) { + String uuid = CursorHelper.getString(cursor, Account.UUID); + String password = CursorHelper.getString(cursor, Account.PASSWORD); + String encryptedPassword = SimpleCryptoUtil.encrypt(Account.PW_SEED, password); + ContentValues values = new ContentValues(); + values.put(Account.PASSWORD, encryptedPassword); + db.update(Account.TABLENAME, values, Account.UUID + "=?", new String[] {uuid}); + } + cursor.close(); + } } } diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java new file mode 100644 index 00000000..0a8c80d1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java @@ -0,0 +1,110 @@ +package de.thedevstack.conversationsplus.utils; + + +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * + */ +public final class SimpleCryptoUtil { + + public static String encrypt(String seed, String cleartext) { + String result = null; + if (null != seed && null != cleartext) { + try { + byte[] rawKey = getRawKey(seed.getBytes()); + byte[] encryptedBytes = encrypt(rawKey, cleartext.getBytes()); + result = toHex(encryptedBytes); + } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException e) { + // FIXME + } + } + + return result; + } + + public static String decrypt(String seed, String encrypted) { + String result = null; + if (null != seed && null != encrypted) { + try { + byte[] rawKey = getRawKey(seed.getBytes()); + byte[] enc = toByte(encrypted); + byte[] decryptedBytes = decrypt(rawKey, enc); + result = new String(decryptedBytes); + } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException e) { + // FIXME + } + } + + return result; + } + + private static byte[] encrypt(byte[] raw, byte[] clear) throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + byte[] encrypted = doCipherOperation(raw, clear, Cipher.ENCRYPT_MODE); + return encrypted; + } + + private static byte[] decrypt(byte[] raw, byte[] encrypted) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + byte[] decrypted = doCipherOperation(raw, encrypted, Cipher.DECRYPT_MODE); + return decrypted; + } + + private static byte[] doCipherOperation(byte[] raw, byte[] input, int mode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(mode, skeySpec); + + return cipher.doFinal(input); + } + + private static byte[] getRawKey(byte[] seed) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] md5Bytes = md.digest(seed); // 128 Bit = 16 byte + SecretKey skey = new SecretKeySpec(md5Bytes, "AES"); + byte[] raw = skey.getEncoded(); + return raw; + } + + public static byte[] toByte(String hexString) { + int len = hexString.length() / 2; + byte[] result = new byte[len]; + for (int i = 0; i < len; i++) { + result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue(); + } + return result; + } + + public static String toHex(byte[] buf) { + if (buf == null) { + return null; + } + StringBuffer result = new StringBuffer(2 * buf.length); + for (int i = 0; i < buf.length; i++) { + appendHex(result, buf[i]); + } + return result.toString(); + } + + private final static String HEX = "0123456789ABCDEF"; + + private static void appendHex(StringBuffer sb, byte b) { + sb.append(HEX.charAt((b >> 4) & 0x0f)).append(HEX.charAt(b & 0x0f)); + } + + /** + * private constructor to avoid instantiation + */ + private SimpleCryptoUtil() { + // private constructor to avoid instantiation + } +} + diff --git a/src/test/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtilTest.java b/src/test/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtilTest.java new file mode 100644 index 00000000..7e71a3bf --- /dev/null +++ b/src/test/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtilTest.java @@ -0,0 +1,136 @@ +package de.thedevstack.conversationsplus.utils; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Created by steckbrief on 10.07.2016. + */ +public class SimpleCryptoUtilTest { + @Test + public void testEncryptWithNullSeed() { + String seed = null; + String cleartext = "Test123"; + String expectedResult = null; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithWhitespaceSeed() { + String seed = " "; + String cleartext = "Test123"; + String expectedResult = "3E7721EF6F8D9E47403AC03F45341E5C"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithZeroLengthSeed() { + String seed = ""; + String cleartext = "Test123"; + String expectedResult = "BEFF4A6800B91AE505672CA238D7C72F"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithNullCleartext() { + String seed = "asdasdadasd"; + String cleartext = null; + String expectedResult = null; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithWhitespaceCleartext() { + String seed = "asdasdadasd"; + String cleartext = " "; + String expectedResult = "1C476E5E4E66809AB6C91EEF9C93A32F"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithZeroLengthCleartext() { + String seed = "asdasdadasd"; + String cleartext = ""; + String expectedResult = "22BD1DA9232CA6594A466B419B2AFB8C"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncrypt() { + String seed = "asdasdadasd"; + String cleartext = "Test123"; + String expectedResult = "E1D9C96AAF09434D915A2F0C147CBF64"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithNullSeed() { + String seed = null; + String encryptedText = "E1D9C96AAF09434D915A2F0C147CBF64"; + String expectedResult = null; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithWhitespaceSeed() { + String seed = " "; + String encryptedText = "3E7721EF6F8D9E47403AC03F45341E5C"; + String expectedResult = "Test123"; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithZeroLengthSeed() { + String seed = ""; + String encryptedText = "BEFF4A6800B91AE505672CA238D7C72F"; + String expectedResult = "Test123"; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithNullEncryptedText() { + String seed = "asdasdadasd"; + String encryptedText = null; + String expectedResult = null; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithWhitespaceEncryptedText() { + String seed = "asdasdadasd"; + String encryptedText = " "; + String expectedResult = ""; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithZeroLengthEncryptedText() { + String seed = "asdasdadasd"; + String encryptedText = ""; + String expectedResult = ""; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecrypt() { + String seed = "asdasdadasd"; + String encryptedText = "E1D9C96AAF09434D915A2F0C147CBF64"; + String expectedResult = "Test123"; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } +} -- cgit v1.2.3 From 3ed7cb54e5858afaadc3f7ec5bc01edb61e1428e Mon Sep 17 00:00:00 2001 From: steckbrief Date: Mon, 22 Aug 2016 21:30:36 +0200 Subject: Basic filetransfer http delete implementation; Exceptions for IqPacketError added --- build.gradle | 4 +- libs/3rdParty/okhttp-3.4.1.jar | Bin 0 -> 344481 bytes libs/3rdParty/okio-1.9.0.jar | Bin 0 -> 76036 bytes .../ConversationsPlusApplication.java | 8 ++ .../conversationsplus/entities/Message.java | 3 + .../conversationsplus/entities/RemoteFile.java | 37 +++++++++ .../conversationsplus/http/HttpClient.java | 81 ++++++++++++++++++++ .../filetransfer/http/DeleteRemoteFile.java | 22 ++++++ .../filetransfer/http/DeleteRemoteFileService.java | 30 ++++++++ .../filetransfer/http/DeleteTokenReceived.java | 83 +++++++++++++++++++++ .../conversationsplus/ui/ConversationFragment.java | 11 ++- .../conversationsplus/utils/MessageUtil.java | 10 +++ .../xmpp/AbstractIqPacketParser.java | 43 ----------- .../conversationsplus/xmpp/IqPacketParser.java | 51 +++++++++++++ .../exceptions/BadRequestIqErrorException.java | 17 +++++ .../exceptions/InternalServerErrorException.java | 17 +++++ .../xmpp/exceptions/IqPacketErrorException.java | 20 +++++ .../exceptions/ServiceUnavailableException.java | 17 +++++ .../exceptions/UndefinedConditionException.java | 17 +++++ .../filetransfer/http/DeleteSlotPacketParser.java | 28 +++++++ .../filetransfer/http/DeleteSlotRequestPacket.java | 33 ++++++++ .../xmpp/filetransfer/http/FileTransferHttp.java | 8 ++ ...ansferHttpDeleteSlotRequestPacketGenerator.java | 39 ++++++++++ .../xmpp/utils/ErrorIqPacketExceptionHelper.java | 44 +++++++++++ src/main/res/menu/message_context.xml | 4 + src/main/res/values/strings.xml | 1 + 26 files changed, 581 insertions(+), 47 deletions(-) create mode 100644 libs/3rdParty/okhttp-3.4.1.jar create mode 100644 libs/3rdParty/okio-1.9.0.jar create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/RemoteFile.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFile.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/AbstractIqPacketParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/IqPacketParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/BadRequestIqErrorException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/InternalServerErrorException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/IqPacketErrorException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/ServiceUnavailableException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UndefinedConditionException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotPacketParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotRequestPacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttp.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttpDeleteSlotRequestPacketGenerator.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/utils/ErrorIqPacketExceptionHelper.java diff --git a/build.gradle b/build.gradle index f58ac6df..1218e605 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ apply plugin: 'com.android.application' repositories { flatDir { - dirs 'libs/3rdParty/jcenter', 'libs/3rdParty/maven' + dirs 'libs/3rdParty/jcenter', 'libs/3rdParty/maven', 'libs/3rdParty' } maven { url "http://snippets.thedevstack.de/mvn" @@ -48,6 +48,8 @@ dependencies { compile name: 'android-crop-1.0.1', ext: 'aar' compile name: 'roundedimageview-2.2.0', ext: 'aar' compile name: 'openpgp-api-10.0', ext: 'aar' // loaded from jcenter + compile name: 'okio-1.9.0' + compile name: 'okhttp-3.4.1' // Local modules compile project(':libs:MemorizingTrustManager') diff --git a/libs/3rdParty/okhttp-3.4.1.jar b/libs/3rdParty/okhttp-3.4.1.jar new file mode 100644 index 00000000..e31f2486 Binary files /dev/null and b/libs/3rdParty/okhttp-3.4.1.jar differ diff --git a/libs/3rdParty/okio-1.9.0.jar b/libs/3rdParty/okio-1.9.0.jar new file mode 100644 index 00000000..3c42b934 Binary files /dev/null and b/libs/3rdParty/okio-1.9.0.jar differ diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java index 26854205..8f970a09 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java @@ -10,6 +10,7 @@ import java.io.File; import java.security.SecureRandom; import de.duenndns.ssl.MemorizingTrustManager; +import de.thedevstack.conversationsplus.http.HttpClient; import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager; @@ -18,6 +19,7 @@ import de.thedevstack.conversationsplus.services.filetransfer.jingle.JingleFileT import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.utils.PRNGFixes; import de.thedevstack.conversationsplus.utils.SerialSingleThreadExecutor; +import okhttp3.OkHttpClient; /** * This class is used to provide static access to the applicationcontext. @@ -46,6 +48,7 @@ public class ConversationsPlusApplication extends Application { FileBackend.init(); FileTransferManager.init(new HttpUploadFileTransferService(), new JingleFileTransferService()); HttpConnectionManager.init(); + HttpClient.init(); } /** @@ -147,6 +150,11 @@ public class ConversationsPlusApplication extends Application { getInstance().setMemorizingTrustManager(tm); } + private static void initHttpClient() { + OkHttpClient client = new OkHttpClient.Builder() + .build(); + } + public static SecureRandom getSecureRandom() { return getInstance().secureRandom; } diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java index b0a5bb3b..f081e82a 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java @@ -27,6 +27,9 @@ public class Message extends AbstractEntity { public static final int STATUS_SEND_RECEIVED = 7; public static final int STATUS_SEND_DISPLAYED = 8; + public static final int STATUS_REMOTE_FILE_DELETE_FAILED = 101; + public static final int STATUS_REMOTE_FILE_DELETED = 102; + public static final int ENCRYPTION_NONE = 0; public static final int ENCRYPTION_PGP = 1; public static final int ENCRYPTION_OTR = 2; diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/RemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/entities/RemoteFile.java new file mode 100644 index 00000000..76c1dbc9 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/RemoteFile.java @@ -0,0 +1,37 @@ +package de.thedevstack.conversationsplus.entities; + +import android.support.annotation.NonNull; + +import java.io.Serializable; + +/** + * Created by steckbrief on 22.08.2016. + */ +public class RemoteFile implements Serializable { + private static final long serialVersionUID = 34564871234564L; + private final String path; + + public RemoteFile(@NonNull String path) { + this.path = path; + } + + public String getPath() { + return path; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RemoteFile that = (RemoteFile) o; + + return path.equals(that.path); + + } + + @Override + public int hashCode() { + return path.hashCode(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java new file mode 100644 index 00000000..7e12a890 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java @@ -0,0 +1,81 @@ +package de.thedevstack.conversationsplus.http; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.SSLSocketHelper; +import okhttp3.OkHttpClient; + +/** + * Created by steckbrief on 22.08.2016. + */ +public final class HttpClient { + private static HttpClient INSTANCE; + private boolean interactive = false; + private OkHttpClient client; + + public static void init() { + INSTANCE = new HttpClient(); + } + + public static synchronized OkHttpClient getClient(boolean interactive) { + if (INSTANCE.interactive != interactive) { + INSTANCE.interactive = interactive; + INSTANCE.buildClient(); + } + return INSTANCE.client; + } + + private HttpClient() { + this.buildClient(); + } + + private void buildClient() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + this.initTrustManager(builder); + this.client = builder.build(); + } + + public void initTrustManager(final OkHttpClient.Builder builder) { + final X509TrustManager trustManager; + final HostnameVerifier hostnameVerifier; + if (interactive) { + trustManager = ConversationsPlusApplication.getMemorizingTrustManager(); + hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager().wrapHostnameVerifier( + new StrictHostnameVerifier()); + } else { + trustManager = ConversationsPlusApplication.getMemorizingTrustManager() + .getNonInteractive(); + hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager() + .wrapHostnameVerifierNonInteractive( + new StrictHostnameVerifier()); + } + try { + final SSLContext sc = SSLSocketHelper.getSSLContext(); + sc.init(null, new X509TrustManager[]{trustManager}, + ConversationsPlusApplication.getSecureRandom()); + + final SSLSocketFactory sf = sc.getSocketFactory(); + final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( + sf.getSupportedCipherSuites()); + if (cipherSuites.length > 0) { + sc.getDefaultSSLParameters().setCipherSuites(cipherSuites); + + } + + builder.sslSocketFactory(sf, trustManager); + builder.hostnameVerifier(hostnameVerifier); + } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFile.java new file mode 100644 index 00000000..9f6b3bbd --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFile.java @@ -0,0 +1,22 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http; + +import android.support.annotation.NonNull; + +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.RemoteFile; + +/** + * Created by steckbrief on 22.08.2016. + */ +public class DeleteRemoteFile extends RemoteFile { + private final Message message; + + public DeleteRemoteFile(@NonNull String path, @NonNull Message message) { + super(path); + this.message = message; + } + + public Message getMessage() { + return message; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java new file mode 100644 index 00000000..57f408cc --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java @@ -0,0 +1,30 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.utils.XmppSendUtil; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttpDeleteSlotRequestPacketGenerator; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * Created by steckbrief on 21.08.2016. + */ +public class DeleteRemoteFileService { + public void deleteRemoteFile(Message message) { + if (message.isHttpUploaded()) { + String path = message.getBody(); + if (message.hasFileOnRemoteHost()) { + path = message.getFileParams().url.toString(); + } + + DeleteRemoteFile remoteFile = new DeleteRemoteFile(path, message); + Account account = message.getConversation().getAccount(); + Jid host = account.getXmppConnection().findDiscoItemByFeature(FileTransferHttp.NAMESPACE); + IqPacket request = FileTransferHttpDeleteSlotRequestPacketGenerator.generate(host, path); + + XmppSendUtil.sendIqPacket(account, request, new DeleteTokenReceived(remoteFile)); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java new file mode 100644 index 00000000..fb83219b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java @@ -0,0 +1,83 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.http.HttpClient; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.DeleteSlotPacketParser; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Created by steckbrief on 21.08.2016. + */ +public class DeleteTokenReceived implements OnIqPacketReceived { + private static final String HEADER_NAME_DELETE_TOKEN = "X-FILETRANSFER-HTTP-DELETE-TOKEN"; + private final DeleteRemoteFile remoteFile; + + public DeleteTokenReceived(DeleteRemoteFile remoteFile) { + this.remoteFile = remoteFile; + } + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + try { + String url = this.remoteFile.getPath(); + String deleteToken = DeleteSlotPacketParser.parseDeleteToken(packet); + Logging.d("filetransfer.http.delete", "Got delete token '" + deleteToken + "' for remote file '" + remoteFile.getPath() + "'"); + OkHttpClient client = HttpClient.getClient(true); + Request request = new Request.Builder() + .url(url) + .addHeader(HEADER_NAME_DELETE_TOKEN, deleteToken) + .delete() + .build(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + Logging.e("filetransfer.http.delete", "Error while connecting to '" + call.request().url() + "': " + e.getMessage()); + MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (response.isSuccessful()) { + MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETED); + Logging.i("filetransfer.http.delete", "Remote file successfully deleted '" + remoteFile.getPath() + "'"); + } else { + String detailedMessage = response.body().string(); + switch (response.code()) { + case 403: + case 404: + case 500: + try { + JSONObject jsonObject = new JSONObject(detailedMessage); + detailedMessage = jsonObject.getString("msg"); + } catch (JSONException e) { + Logging.e("filetransfer.http.delete", "Failed to get error message from expected json response: " + detailedMessage); + } + break; + } + Logging.e("filetransfer.http.delete", "Could not delete remote file '" + remoteFile.getPath() + "'. Response Code: " + response.code() + ", details: " + detailedMessage); + MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); + } + } + }); + + } catch (XmppException e) { + Logging.e("filetransfer.http.delete", "Error while trying to get the delete token: " + e.getMessage()); + MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java index d6e6f34c..934f16b6 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java @@ -8,7 +8,6 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.support.annotation.Nullable; @@ -24,8 +23,6 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ImageButton; @@ -48,6 +45,7 @@ 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.services.filetransfer.http.DeleteRemoteFileService; import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; @@ -576,12 +574,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa deleteFile.setVisible(true); deleteFile.setTitle(activity.getString(R.string.delete_x_file,UIHelper.getFileDescriptionString(activity, m))); } + if (m.isHttpUploaded() && (MessageUtil.isMessageSent(m) || m.getStatus() == Message.STATUS_REMOTE_FILE_DELETE_FAILED)) { + MenuItem deleteRemoteFile = menu.findItem(R.id.msg_ctx_menu_delete_remote_file); + deleteRemoteFile.setVisible(true); + } } } @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { + case R.id.msg_ctx_menu_delete_remote_file: + new DeleteRemoteFileService().deleteRemoteFile(selectedMessage); + return true; case R.id.msg_ctx_mnu_details: new MessageDetailsDialog(getActivity(), selectedMessage).show(); return true; diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java index 67433b31..626b1bf3 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java @@ -18,6 +18,16 @@ import de.thedevstack.conversationsplus.persistance.FileBackend; */ public final class MessageUtil { + public static boolean isMessageSent(Message message) { + switch (message.getStatus()) { + case Message.STATUS_SEND: + case Message.STATUS_SEND_DISPLAYED: + case Message.STATUS_SEND_RECEIVED: + return true; + default: + return false; + } + } public static boolean markMessage(Conversation conversation, String uuid, int status) { if (uuid == null) { diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/AbstractIqPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/AbstractIqPacketParser.java deleted file mode 100644 index d777cb64..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/AbstractIqPacketParser.java +++ /dev/null @@ -1,43 +0,0 @@ -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/IqPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/IqPacketParser.java new file mode 100644 index 00000000..7c198ce9 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/IqPacketParser.java @@ -0,0 +1,51 @@ +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 IqPacketParser { + public static Element findRequiredChild(Element element, String elementName, String namespace) throws MissingRequiredElementException { + Element child = findChild(element, elementName, namespace); + if (child == null) { + throw new MissingRequiredElementException(elementName, namespace, element); + } + return child; + } + + public static Element findChild(Element element, String elementName, String namespace) { + if (null == element) { + return null; + } + return element.findChild(elementName, namespace); + } + + public 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; + } + + public static String findRequiredChildContent(Element element, String elementName, String namespace) throws MissingRequiredContentException { + String childContent = findChildContent(element, elementName, namespace); + if (null == childContent) { + throw new MissingRequiredContentException(elementName, namespace, element); + } + return childContent; + } + + public static String findChildContent(Element element, String elementName, String namespace) { + if (null == element) { + return null; + } + return element.findChildContent(elementName, namespace); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/BadRequestIqErrorException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/BadRequestIqErrorException.java new file mode 100644 index 00000000..69fb893b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/BadRequestIqErrorException.java @@ -0,0 +1,17 @@ +package de.thedevstack.conversationsplus.xmpp.exceptions; + +import de.thedevstack.conversationsplus.xml.Element; + +/** + * Created by steckbrief on 22.08.2016. + */ +public class BadRequestIqErrorException extends IqPacketErrorException { + public BadRequestIqErrorException(Element context, String errorMessage) { + super(context, errorMessage); + } + + @Override + public String getMessage() { + return "Bad Request: " + super.getMessage(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/InternalServerErrorException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/InternalServerErrorException.java new file mode 100644 index 00000000..7925c423 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/InternalServerErrorException.java @@ -0,0 +1,17 @@ +package de.thedevstack.conversationsplus.xmpp.exceptions; + +import de.thedevstack.conversationsplus.xml.Element; + +/** + * Created by steckbrief on 22.08.2016. + */ +public class InternalServerErrorException extends IqPacketErrorException { + public InternalServerErrorException(Element packet, String errorText) { + super(packet, errorText); + } + + @Override + public String getMessage() { + return "Internal Server Error: " + super.getMessage(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/IqPacketErrorException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/IqPacketErrorException.java new file mode 100644 index 00000000..d4d932b7 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/IqPacketErrorException.java @@ -0,0 +1,20 @@ +package de.thedevstack.conversationsplus.xmpp.exceptions; + +import de.thedevstack.conversationsplus.xml.Element; + +/** + * Created by steckbrief on 22.08.2016. + */ +public class IqPacketErrorException extends XmppException { + private final String iqErrorText; + + public IqPacketErrorException(Element context, String errorMessage) { + super(context); + this.iqErrorText = errorMessage; + } + + @Override + public String getMessage() { + return this.iqErrorText + " in context: " + this.getContext(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/ServiceUnavailableException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/ServiceUnavailableException.java new file mode 100644 index 00000000..7e503aa9 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/ServiceUnavailableException.java @@ -0,0 +1,17 @@ +package de.thedevstack.conversationsplus.xmpp.exceptions; + +import de.thedevstack.conversationsplus.xml.Element; + +/** + * Created by steckbrief on 22.08.2016. + */ +public class ServiceUnavailableException extends IqPacketErrorException { + public ServiceUnavailableException(Element context, String errorMessage) { + super(context, errorMessage); + } + + @Override + public String getMessage() { + return "Service unavailable: " + super.getMessage(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UndefinedConditionException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UndefinedConditionException.java new file mode 100644 index 00000000..38e1f6d2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UndefinedConditionException.java @@ -0,0 +1,17 @@ +package de.thedevstack.conversationsplus.xmpp.exceptions; + +import de.thedevstack.conversationsplus.xml.Element; + +/** + * Created by steckbrief on 22.08.2016. + */ +public class UndefinedConditionException extends IqPacketErrorException { + public UndefinedConditionException(Element packet, String errorText) { + super(packet, errorText); + } + + @Override + public String getMessage() { + return "Undefined Condition: " + super.getMessage(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotPacketParser.java new file mode 100644 index 00000000..d7e136fc --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotPacketParser.java @@ -0,0 +1,28 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.IqPacketParser; +import de.thedevstack.conversationsplus.xmpp.exceptions.UnexpectedIqPacketTypeException; +import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +import de.thedevstack.conversationsplus.xmpp.utils.ErrorIqPacketExceptionHelper; + +/** + * Created by steckbrief on 21.08.2016. + */ +public class DeleteSlotPacketParser extends IqPacketParser { + public static String parseDeleteToken(IqPacket packet) throws XmppException { + String deletetoken = null; + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element slot = findRequiredChild(packet, "slot", FileTransferHttp.NAMESPACE); + + deletetoken = findRequiredChildContent(slot, "deletetoken"); + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + ErrorIqPacketExceptionHelper.throwIqErrorException(packet); + } else { + throw new UnexpectedIqPacketTypeException(packet, packet.getType(), IqPacket.TYPE.RESULT, IqPacket.TYPE.ERROR); + } + + return deletetoken; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotRequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotRequestPacket.java new file mode 100644 index 00000000..6adc3aac --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotRequestPacket.java @@ -0,0 +1,33 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * Created by steckbrief on 21.08.2016. + */ +public class DeleteSlotRequestPacket extends IqPacket { + public static final String ELEMENT_NAME = "request"; + public static final String FILEURL_ELEMENT_NAME = "fileurl"; + private Element requestElement; + private String fileurl; + + private DeleteSlotRequestPacket() { + super(TYPE.GET); + this.requestElement = super.addChild(DeleteSlotRequestPacket.ELEMENT_NAME, FileTransferHttp.NAMESPACE); + this.requestElement.setAttribute("type", "delete"); + } + + public DeleteSlotRequestPacket(String fileurl) { + this(); + this.setFileURL(fileurl); + } + + public void setFileURL(String fileurl) { + if (null == fileurl || fileurl.isEmpty()) { + throw new IllegalArgumentException("fileurl must not be null or empty."); + } + this.fileurl = fileurl; + this.requestElement.addChild(FILEURL_ELEMENT_NAME).setContent(fileurl); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttp.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttp.java new file mode 100644 index 00000000..28f4f870 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttp.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http; + +/** + * Created by steckbrief on 21.08.2016. + */ +public interface FileTransferHttp { + String NAMESPACE = "urn:xmpp:filetransfer:http"; +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttpDeleteSlotRequestPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttpDeleteSlotRequestPacketGenerator.java new file mode 100644 index 00000000..bc4380fe --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttpDeleteSlotRequestPacketGenerator.java @@ -0,0 +1,39 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http; + +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * Created by steckbrief on 21.08.2016. + */ +public final class FileTransferHttpDeleteSlotRequestPacketGenerator { + /** + * Generates the IqPacket to request a slot to delete a file previously uploaded via http upload. + * The attributes from and id are not set in here - this is added while sending the packet. + *
+     * 
+     *   
+     *     http://upload.montague.tld/files/1e56ee17-ee4c-4a9c-aedd-cb09cb3984a7/my_juliet.png
+     *   
+     * 
+     * 
+ * @param host the jid of the host to request a slot from + * @param fileurl the URL of the file to be deleted + * @return the IqPacket + */ + public static IqPacket generate(Jid host, String fileurl) { + DeleteSlotRequestPacket packet = new DeleteSlotRequestPacket(fileurl); + packet.setTo(host); + return packet; + } + + /** + * Utility class - avoid instantiation + */ + private FileTransferHttpDeleteSlotRequestPacketGenerator() { + // Helper class - avoid instantiation + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/utils/ErrorIqPacketExceptionHelper.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/utils/ErrorIqPacketExceptionHelper.java new file mode 100644 index 00000000..f02d8d46 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/utils/ErrorIqPacketExceptionHelper.java @@ -0,0 +1,44 @@ +package de.thedevstack.conversationsplus.xmpp.utils; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.IqPacketParser; +import de.thedevstack.conversationsplus.xmpp.exceptions.BadRequestIqErrorException; +import de.thedevstack.conversationsplus.xmpp.exceptions.InternalServerErrorException; +import de.thedevstack.conversationsplus.xmpp.exceptions.IqPacketErrorException; +import de.thedevstack.conversationsplus.xmpp.exceptions.ServiceUnavailableException; +import de.thedevstack.conversationsplus.xmpp.exceptions.UndefinedConditionException; + +/** + * Created by steckbrief on 22.08.2016. + */ +public final class ErrorIqPacketExceptionHelper { + private final static String ERROR_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-stanzas"; + + public static void throwIqErrorException(Element packet) throws IqPacketErrorException { + if (hasErrorElement(packet, "bad-request")) { + throw new BadRequestIqErrorException(packet, getErrorText(packet)); + } + if (hasErrorElement(packet, "service-unavailable")) { + throw new ServiceUnavailableException(packet, getErrorText(packet)); + } + if (hasErrorElement(packet, "internal-server-error")) { + throw new InternalServerErrorException(packet, getErrorText(packet)); + } + if (hasErrorElement(packet, "undefined-condition")) { + throw new UndefinedConditionException(packet, getErrorText(packet)); + } + throw new IqPacketErrorException(packet, "Unknown error packet."); + } + + private static boolean hasErrorElement(Element packet, String elementName) { + return null != IqPacketParser.findChild(packet, elementName, ERROR_NAMESPACE); + } + + private static String getErrorText(Element packet) { + return IqPacketParser.findChildContent(packet, "text", ERROR_NAMESPACE); + } + + private ErrorIqPacketExceptionHelper() { + + } +} diff --git a/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml index 0c7d8eef..da7f0636 100644 --- a/src/main/res/menu/message_context.xml +++ b/src/main/res/menu/message_context.xml @@ -36,4 +36,8 @@ android:id="@+id/delete_file" android:title="@string/delete_x_file" android:visible="false"/> + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 3da33010..22b6c490 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -674,4 +674,5 @@ Security error: Invalid file access No application found to share URI Share URI with… + Delete remote file -- cgit v1.2.3 From e7089c55ac72f716e17203a9b8732b2b5c82c150 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Mon, 22 Aug 2016 21:30:57 +0200 Subject: Improved error handling for httpupload --- .../conversationsplus/xmpp/httpupload/SlotPacketParser.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java index 0630449a..655d8759 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java @@ -1,27 +1,30 @@ package de.thedevstack.conversationsplus.xmpp.httpupload; import de.thedevstack.conversationsplus.xml.Element; -import de.thedevstack.conversationsplus.xmpp.AbstractIqPacketParser; +import de.thedevstack.conversationsplus.xmpp.IqPacketParser; import de.thedevstack.conversationsplus.xmpp.exceptions.UnexpectedIqPacketTypeException; import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +import de.thedevstack.conversationsplus.xmpp.utils.ErrorIqPacketExceptionHelper; /** * */ -public final class SlotPacketParser extends AbstractIqPacketParser { +public final class SlotPacketParser extends IqPacketParser { public static HttpUploadSlot parseGetAndPutUrl(IqPacket packet) throws XmppException { + HttpUploadSlot httpUploadSlot = null; 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); + httpUploadSlot = new HttpUploadSlot(getUrl, putUrl); } else if (packet.getType() == IqPacket.TYPE.ERROR) { - throw new XmppException(); // Do proper handling of error cases + ErrorIqPacketExceptionHelper.throwIqErrorException(packet); } else { throw new UnexpectedIqPacketTypeException(packet, packet.getType(), IqPacket.TYPE.RESULT, IqPacket.TYPE.ERROR); } + return httpUploadSlot; } } -- cgit v1.2.3 From 2d462a746ea6e733f9f2836b181795c80de5aae5 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 23 Aug 2016 09:46:51 +0200 Subject: xmpp.httpupload moved to new namespace xmpp.filetransfer.http.upload; delete parts of xmpp.filetransfer.http moved to .delete package --- .../http/HttpUploadConnection.java | 13 ++---- .../filetransfer/http/DeleteRemoteFileService.java | 2 +- .../filetransfer/http/DeleteTokenReceived.java | 2 +- .../httpupload/HttpFileTransferEntity.java | 2 +- .../filetransfer/httpupload/HttpFileUploader.java | 5 +- .../httpupload/HttpUploadFileTransferService.java | 4 +- .../httpupload/HttpUploadSlotRequestReceived.java | 4 +- .../filetransfer/http/DeleteSlotPacketParser.java | 28 ------------ .../filetransfer/http/DeleteSlotRequestPacket.java | 33 -------------- ...ansferHttpDeleteSlotRequestPacketGenerator.java | 39 ---------------- .../http/delete/DeleteSlotPacketParser.java | 29 ++++++++++++ .../http/delete/DeleteSlotRequestPacket.java | 34 ++++++++++++++ ...ansferHttpDeleteSlotRequestPacketGenerator.java | 39 ++++++++++++++++ .../xmpp/filetransfer/http/upload/HttpUpload.java | 9 ++++ .../HttpUploadRequestSlotPacketGenerator.java | 46 +++++++++++++++++++ .../filetransfer/http/upload/HttpUploadSlot.java | 22 +++++++++ .../filetransfer/http/upload/SlotPacketParser.java | 30 ++++++++++++ .../http/upload/SlotRequestPacket.java | 53 ++++++++++++++++++++++ .../xmpp/httpupload/HttpUpload.java | 9 ---- .../HttpUploadRequestSlotPacketGenerator.java | 46 ------------------- .../xmpp/httpupload/HttpUploadSlot.java | 22 --------- .../xmpp/httpupload/SlotPacketParser.java | 30 ------------ .../xmpp/httpupload/SlotRequestPacket.java | 53 ---------------------- 23 files changed, 274 insertions(+), 280 deletions(-) delete mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotPacketParser.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotRequestPacket.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttpDeleteSlotRequestPacketGenerator.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotPacketParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotRequestPacket.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/FileTransferHttpDeleteSlotRequestPacketGenerator.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUpload.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadRequestSlotPacketGenerator.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadSlot.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotPacketParser.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotRequestPacket.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUpload.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadRequestSlotPacketGenerator.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadSlot.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotRequestPacket.java diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java index 7c791e70..3c49d083 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java @@ -29,18 +29,13 @@ import de.thedevstack.conversationsplus.services.AbstractConnectionManager; 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.filetransfer.http.upload.HttpUpload; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUploadRequestSlotPacketGenerator; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUploadSlot; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.SlotPacketParser; import de.thedevstack.conversationsplus.xmpp.jid.Jid; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java index 57f408cc..565143be 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java @@ -4,7 +4,7 @@ import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.utils.XmppSendUtil; import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttpDeleteSlotRequestPacketGenerator; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.FileTransferHttpDeleteSlotRequestPacketGenerator; import de.thedevstack.conversationsplus.xmpp.jid.Jid; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java index fb83219b..cce0b713 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java @@ -12,7 +12,7 @@ import de.thedevstack.conversationsplus.http.HttpClient; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.DeleteSlotPacketParser; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.DeleteSlotPacketParser; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; import okhttp3.Call; import okhttp3.Callback; 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 index a8e5734f..f6596b0c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java @@ -11,7 +11,7 @@ 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; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUploadSlot; /** * 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 index 8edc5be7..705dd23c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java @@ -13,19 +13,16 @@ 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; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUpload; public class HttpFileUploader implements Runnable { private static final String HTTP_METHOD = "PUT"; 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 index fb150a92..6fd458a9 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java @@ -15,8 +15,8 @@ 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.filetransfer.http.upload.HttpUpload; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUploadRequestSlotPacketGenerator; import de.thedevstack.conversationsplus.xmpp.jid.Jid; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; 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 index 462a370c..75e793f1 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java @@ -5,8 +5,8 @@ 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.filetransfer.http.upload.HttpUploadSlot; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.SlotPacketParser; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; /** diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotPacketParser.java deleted file mode 100644 index d7e136fc..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotPacketParser.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.thedevstack.conversationsplus.xmpp.filetransfer.http; - -import de.thedevstack.conversationsplus.xml.Element; -import de.thedevstack.conversationsplus.xmpp.IqPacketParser; -import de.thedevstack.conversationsplus.xmpp.exceptions.UnexpectedIqPacketTypeException; -import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; -import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; -import de.thedevstack.conversationsplus.xmpp.utils.ErrorIqPacketExceptionHelper; - -/** - * Created by steckbrief on 21.08.2016. - */ -public class DeleteSlotPacketParser extends IqPacketParser { - public static String parseDeleteToken(IqPacket packet) throws XmppException { - String deletetoken = null; - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element slot = findRequiredChild(packet, "slot", FileTransferHttp.NAMESPACE); - - deletetoken = findRequiredChildContent(slot, "deletetoken"); - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - ErrorIqPacketExceptionHelper.throwIqErrorException(packet); - } else { - throw new UnexpectedIqPacketTypeException(packet, packet.getType(), IqPacket.TYPE.RESULT, IqPacket.TYPE.ERROR); - } - - return deletetoken; - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotRequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotRequestPacket.java deleted file mode 100644 index 6adc3aac..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/DeleteSlotRequestPacket.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.thedevstack.conversationsplus.xmpp.filetransfer.http; - -import de.thedevstack.conversationsplus.xml.Element; -import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; - -/** - * Created by steckbrief on 21.08.2016. - */ -public class DeleteSlotRequestPacket extends IqPacket { - public static final String ELEMENT_NAME = "request"; - public static final String FILEURL_ELEMENT_NAME = "fileurl"; - private Element requestElement; - private String fileurl; - - private DeleteSlotRequestPacket() { - super(TYPE.GET); - this.requestElement = super.addChild(DeleteSlotRequestPacket.ELEMENT_NAME, FileTransferHttp.NAMESPACE); - this.requestElement.setAttribute("type", "delete"); - } - - public DeleteSlotRequestPacket(String fileurl) { - this(); - this.setFileURL(fileurl); - } - - public void setFileURL(String fileurl) { - if (null == fileurl || fileurl.isEmpty()) { - throw new IllegalArgumentException("fileurl must not be null or empty."); - } - this.fileurl = fileurl; - this.requestElement.addChild(FILEURL_ELEMENT_NAME).setContent(fileurl); - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttpDeleteSlotRequestPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttpDeleteSlotRequestPacketGenerator.java deleted file mode 100644 index bc4380fe..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttpDeleteSlotRequestPacketGenerator.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.thedevstack.conversationsplus.xmpp.filetransfer.http; - -import de.thedevstack.conversationsplus.xmpp.jid.Jid; -import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; - -/** - * Created by steckbrief on 21.08.2016. - */ -public final class FileTransferHttpDeleteSlotRequestPacketGenerator { - /** - * Generates the IqPacket to request a slot to delete a file previously uploaded via http upload. - * The attributes from and id are not set in here - this is added while sending the packet. - *
-     * 
-     *   
-     *     http://upload.montague.tld/files/1e56ee17-ee4c-4a9c-aedd-cb09cb3984a7/my_juliet.png
-     *   
-     * 
-     * 
- * @param host the jid of the host to request a slot from - * @param fileurl the URL of the file to be deleted - * @return the IqPacket - */ - public static IqPacket generate(Jid host, String fileurl) { - DeleteSlotRequestPacket packet = new DeleteSlotRequestPacket(fileurl); - packet.setTo(host); - return packet; - } - - /** - * Utility class - avoid instantiation - */ - private FileTransferHttpDeleteSlotRequestPacketGenerator() { - // Helper class - avoid instantiation - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotPacketParser.java new file mode 100644 index 00000000..7c011449 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotPacketParser.java @@ -0,0 +1,29 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.IqPacketParser; +import de.thedevstack.conversationsplus.xmpp.exceptions.UnexpectedIqPacketTypeException; +import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +import de.thedevstack.conversationsplus.xmpp.utils.ErrorIqPacketExceptionHelper; + +/** + * Created by steckbrief on 21.08.2016. + */ +public class DeleteSlotPacketParser extends IqPacketParser { + public static String parseDeleteToken(IqPacket packet) throws XmppException { + String deletetoken = null; + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element slot = findRequiredChild(packet, "slot", FileTransferHttp.NAMESPACE); + + deletetoken = findRequiredChildContent(slot, "deletetoken"); + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + ErrorIqPacketExceptionHelper.throwIqErrorException(packet); + } else { + throw new UnexpectedIqPacketTypeException(packet, packet.getType(), IqPacket.TYPE.RESULT, IqPacket.TYPE.ERROR); + } + + return deletetoken; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotRequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotRequestPacket.java new file mode 100644 index 00000000..e389d851 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotRequestPacket.java @@ -0,0 +1,34 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * Created by steckbrief on 21.08.2016. + */ +public class DeleteSlotRequestPacket extends IqPacket { + public static final String ELEMENT_NAME = "request"; + public static final String FILEURL_ELEMENT_NAME = "fileurl"; + private Element requestElement; + private String fileurl; + + private DeleteSlotRequestPacket() { + super(TYPE.GET); + this.requestElement = super.addChild(DeleteSlotRequestPacket.ELEMENT_NAME, FileTransferHttp.NAMESPACE); + this.requestElement.setAttribute("type", "delete"); + } + + public DeleteSlotRequestPacket(String fileurl) { + this(); + this.setFileURL(fileurl); + } + + public void setFileURL(String fileurl) { + if (null == fileurl || fileurl.isEmpty()) { + throw new IllegalArgumentException("fileurl must not be null or empty."); + } + this.fileurl = fileurl; + this.requestElement.addChild(FILEURL_ELEMENT_NAME).setContent(fileurl); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/FileTransferHttpDeleteSlotRequestPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/FileTransferHttpDeleteSlotRequestPacketGenerator.java new file mode 100644 index 00000000..ac7de74e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/FileTransferHttpDeleteSlotRequestPacketGenerator.java @@ -0,0 +1,39 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete; + +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * Created by steckbrief on 21.08.2016. + */ +public final class FileTransferHttpDeleteSlotRequestPacketGenerator { + /** + * Generates the IqPacket to request a slot to delete a file previously uploaded via http upload. + * The attributes from and id are not set in here - this is added while sending the packet. + *
+     * 
+     *   
+     *     http://upload.montague.tld/files/1e56ee17-ee4c-4a9c-aedd-cb09cb3984a7/my_juliet.png
+     *   
+     * 
+     * 
+ * @param host the jid of the host to request a slot from + * @param fileurl the URL of the file to be deleted + * @return the IqPacket + */ + public static IqPacket generate(Jid host, String fileurl) { + DeleteSlotRequestPacket packet = new DeleteSlotRequestPacket(fileurl); + packet.setTo(host); + return packet; + } + + /** + * Utility class - avoid instantiation + */ + private FileTransferHttpDeleteSlotRequestPacketGenerator() { + // Helper class - avoid instantiation + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUpload.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUpload.java new file mode 100644 index 00000000..3bcbd219 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUpload.java @@ -0,0 +1,9 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload; + +/** + * + */ +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/filetransfer/http/upload/HttpUploadRequestSlotPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadRequestSlotPacketGenerator.java new file mode 100644 index 00000000..eaa90ab0 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadRequestSlotPacketGenerator.java @@ -0,0 +1,46 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload; + +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 http://xmpp.org/extensions/xep-0363.html + */ +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. + *
+     * 
+     *   
+     *     my_juliet.png
+     *     23456
+     *     image/jpeg
+     *   
+     * 
+     * 
+ * @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 - optional 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/filetransfer/http/upload/HttpUploadSlot.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadSlot.java new file mode 100644 index 00000000..1e320afe --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadSlot.java @@ -0,0 +1,22 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload; + +/** + * + */ +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/filetransfer/http/upload/SlotPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotPacketParser.java new file mode 100644 index 00000000..85d11b6b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotPacketParser.java @@ -0,0 +1,30 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.IqPacketParser; +import de.thedevstack.conversationsplus.xmpp.exceptions.UnexpectedIqPacketTypeException; +import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +import de.thedevstack.conversationsplus.xmpp.utils.ErrorIqPacketExceptionHelper; + +/** + * + */ +public final class SlotPacketParser extends IqPacketParser { + public static HttpUploadSlot parseGetAndPutUrl(IqPacket packet) throws XmppException { + HttpUploadSlot httpUploadSlot = null; + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element slot = findRequiredChild(packet, "slot", HttpUpload.NAMESPACE); + + String getUrl = findRequiredChildContent(slot, "get"); + String putUrl = findRequiredChildContent(slot, "put"); + + httpUploadSlot = new HttpUploadSlot(getUrl, putUrl); + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + ErrorIqPacketExceptionHelper.throwIqErrorException(packet); + } else { + throw new UnexpectedIqPacketTypeException(packet, packet.getType(), IqPacket.TYPE.RESULT, IqPacket.TYPE.ERROR); + } + return httpUploadSlot; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotRequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotRequestPacket.java new file mode 100644 index 00000000..d0866508 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotRequestPacket.java @@ -0,0 +1,53 @@ +package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload; + +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/httpupload/HttpUpload.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUpload.java deleted file mode 100644 index 28cba280..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUpload.java +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 59851417..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadRequestSlotPacketGenerator.java +++ /dev/null @@ -1,46 +0,0 @@ -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 http://xmpp.org/extensions/xep-0363.html - */ -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. - *
-     * 
-     *   
-     *     my_juliet.png
-     *     23456
-     *     image/jpeg
-     *   
-     * 
-     * 
- * @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 - optional 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 deleted file mode 100644 index 14fe5103..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/HttpUploadSlot.java +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 655d8759..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotPacketParser.java +++ /dev/null @@ -1,30 +0,0 @@ -package de.thedevstack.conversationsplus.xmpp.httpupload; - -import de.thedevstack.conversationsplus.xml.Element; -import de.thedevstack.conversationsplus.xmpp.IqPacketParser; -import de.thedevstack.conversationsplus.xmpp.exceptions.UnexpectedIqPacketTypeException; -import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; -import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; -import de.thedevstack.conversationsplus.xmpp.utils.ErrorIqPacketExceptionHelper; - -/** - * - */ -public final class SlotPacketParser extends IqPacketParser { - public static HttpUploadSlot parseGetAndPutUrl(IqPacket packet) throws XmppException { - HttpUploadSlot httpUploadSlot = null; - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element slot = findRequiredChild(packet, "slot", HttpUpload.NAMESPACE); - - String getUrl = findRequiredChildContent(slot, "get"); - String putUrl = findRequiredChildContent(slot, "put"); - - httpUploadSlot = new HttpUploadSlot(getUrl, putUrl); - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - ErrorIqPacketExceptionHelper.throwIqErrorException(packet); - } else { - throw new UnexpectedIqPacketTypeException(packet, packet.getType(), IqPacket.TYPE.RESULT, IqPacket.TYPE.ERROR); - } - return httpUploadSlot; - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotRequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotRequestPacket.java deleted file mode 100644 index d470d2f5..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpupload/SlotRequestPacket.java +++ /dev/null @@ -1,53 +0,0 @@ -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); - } -} -- cgit v1.2.3 From e91e8a30b1f4a806d77d74871df5af6cdb87ca0e Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 23 Aug 2016 09:49:06 +0200 Subject: services.filetransfer.httpupload moved to new namespace services.filetransfer.http.upload; delete parts of services.filetransfer.http moved to .delete package --- .../ConversationsPlusApplication.java | 2 +- .../services/filetransfer/FileTransferManager.java | 2 +- .../filetransfer/http/DeleteRemoteFile.java | 22 --- .../filetransfer/http/DeleteRemoteFileService.java | 30 ----- .../filetransfer/http/DeleteTokenReceived.java | 83 ------------ .../filetransfer/http/delete/DeleteRemoteFile.java | 22 +++ .../http/delete/DeleteRemoteFileService.java | 30 +++++ .../http/delete/DeleteTokenReceived.java | 83 ++++++++++++ .../http/upload/HttpFileTransferEntity.java | 87 ++++++++++++ .../filetransfer/http/upload/HttpFileUploader.java | 149 +++++++++++++++++++++ .../http/upload/HttpUploadFileTransferService.java | 91 +++++++++++++ .../http/upload/HttpUploadSlotRequestReceived.java | 35 +++++ .../HttpUploadedFileEncryptionUiCallback.java | 34 +++++ .../httpupload/HttpFileTransferEntity.java | 87 ------------ .../filetransfer/httpupload/HttpFileUploader.java | 149 --------------------- .../httpupload/HttpUploadFileTransferService.java | 91 ------------- .../httpupload/HttpUploadSlotRequestReceived.java | 35 ----- .../HttpUploadedFileEncryptionUiCallback.java | 34 ----- .../conversationsplus/ui/ConversationFragment.java | 2 +- 19 files changed, 534 insertions(+), 534 deletions(-) delete mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFile.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadSlotRequestReceived.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadedFileEncryptionUiCallback.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java index 8f970a09..d4e3ebc2 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java @@ -14,7 +14,7 @@ import de.thedevstack.conversationsplus.http.HttpClient; 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.http.upload.HttpUploadFileTransferService; import de.thedevstack.conversationsplus.services.filetransfer.jingle.JingleFileTransferService; import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.utils.PRNGFixes; diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java index 017b88ea..2f9a819b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java @@ -9,7 +9,7 @@ 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.services.filetransfer.http.upload.HttpFileTransferEntity; import de.thedevstack.conversationsplus.utils.MessageUtil; /** diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFile.java deleted file mode 100644 index 9f6b3bbd..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFile.java +++ /dev/null @@ -1,22 +0,0 @@ -package de.thedevstack.conversationsplus.services.filetransfer.http; - -import android.support.annotation.NonNull; - -import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.entities.RemoteFile; - -/** - * Created by steckbrief on 22.08.2016. - */ -public class DeleteRemoteFile extends RemoteFile { - private final Message message; - - public DeleteRemoteFile(@NonNull String path, @NonNull Message message) { - super(path); - this.message = message; - } - - public Message getMessage() { - return message; - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java deleted file mode 100644 index 565143be..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteRemoteFileService.java +++ /dev/null @@ -1,30 +0,0 @@ -package de.thedevstack.conversationsplus.services.filetransfer.http; - -import de.thedevstack.conversationsplus.entities.Account; -import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.utils.XmppSendUtil; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.FileTransferHttpDeleteSlotRequestPacketGenerator; -import de.thedevstack.conversationsplus.xmpp.jid.Jid; -import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; - -/** - * Created by steckbrief on 21.08.2016. - */ -public class DeleteRemoteFileService { - public void deleteRemoteFile(Message message) { - if (message.isHttpUploaded()) { - String path = message.getBody(); - if (message.hasFileOnRemoteHost()) { - path = message.getFileParams().url.toString(); - } - - DeleteRemoteFile remoteFile = new DeleteRemoteFile(path, message); - Account account = message.getConversation().getAccount(); - Jid host = account.getXmppConnection().findDiscoItemByFeature(FileTransferHttp.NAMESPACE); - IqPacket request = FileTransferHttpDeleteSlotRequestPacketGenerator.generate(host, path); - - XmppSendUtil.sendIqPacket(account, request, new DeleteTokenReceived(remoteFile)); - } - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java deleted file mode 100644 index cce0b713..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/DeleteTokenReceived.java +++ /dev/null @@ -1,83 +0,0 @@ -package de.thedevstack.conversationsplus.services.filetransfer.http; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; - -import de.thedevstack.android.logcat.Logging; -import de.thedevstack.conversationsplus.entities.Account; -import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.http.HttpClient; -import de.thedevstack.conversationsplus.utils.MessageUtil; -import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; -import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.DeleteSlotPacketParser; -import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -/** - * Created by steckbrief on 21.08.2016. - */ -public class DeleteTokenReceived implements OnIqPacketReceived { - private static final String HEADER_NAME_DELETE_TOKEN = "X-FILETRANSFER-HTTP-DELETE-TOKEN"; - private final DeleteRemoteFile remoteFile; - - public DeleteTokenReceived(DeleteRemoteFile remoteFile) { - this.remoteFile = remoteFile; - } - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - try { - String url = this.remoteFile.getPath(); - String deleteToken = DeleteSlotPacketParser.parseDeleteToken(packet); - Logging.d("filetransfer.http.delete", "Got delete token '" + deleteToken + "' for remote file '" + remoteFile.getPath() + "'"); - OkHttpClient client = HttpClient.getClient(true); - Request request = new Request.Builder() - .url(url) - .addHeader(HEADER_NAME_DELETE_TOKEN, deleteToken) - .delete() - .build(); - client.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - Logging.e("filetransfer.http.delete", "Error while connecting to '" + call.request().url() + "': " + e.getMessage()); - MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - if (response.isSuccessful()) { - MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETED); - Logging.i("filetransfer.http.delete", "Remote file successfully deleted '" + remoteFile.getPath() + "'"); - } else { - String detailedMessage = response.body().string(); - switch (response.code()) { - case 403: - case 404: - case 500: - try { - JSONObject jsonObject = new JSONObject(detailedMessage); - detailedMessage = jsonObject.getString("msg"); - } catch (JSONException e) { - Logging.e("filetransfer.http.delete", "Failed to get error message from expected json response: " + detailedMessage); - } - break; - } - Logging.e("filetransfer.http.delete", "Could not delete remote file '" + remoteFile.getPath() + "'. Response Code: " + response.code() + ", details: " + detailedMessage); - MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); - } - } - }); - - } catch (XmppException e) { - Logging.e("filetransfer.http.delete", "Error while trying to get the delete token: " + e.getMessage()); - MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); - } - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java new file mode 100644 index 00000000..39a369ef --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java @@ -0,0 +1,22 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.delete; + +import android.support.annotation.NonNull; + +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.RemoteFile; + +/** + * Created by steckbrief on 22.08.2016. + */ +public class DeleteRemoteFile extends RemoteFile { + private final Message message; + + public DeleteRemoteFile(@NonNull String path, @NonNull Message message) { + super(path); + this.message = message; + } + + public Message getMessage() { + return message; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java new file mode 100644 index 00000000..42410ed4 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java @@ -0,0 +1,30 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.delete; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.utils.XmppSendUtil; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.FileTransferHttpDeleteSlotRequestPacketGenerator; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +/** + * Created by steckbrief on 21.08.2016. + */ +public class DeleteRemoteFileService { + public void deleteRemoteFile(Message message) { + if (message.isHttpUploaded()) { + String path = message.getBody(); + if (message.hasFileOnRemoteHost()) { + path = message.getFileParams().url.toString(); + } + + DeleteRemoteFile remoteFile = new DeleteRemoteFile(path, message); + Account account = message.getConversation().getAccount(); + Jid host = account.getXmppConnection().findDiscoItemByFeature(FileTransferHttp.NAMESPACE); + IqPacket request = FileTransferHttpDeleteSlotRequestPacketGenerator.generate(host, path); + + XmppSendUtil.sendIqPacket(account, request, new DeleteTokenReceived(remoteFile)); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java new file mode 100644 index 00000000..a8bef0ed --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java @@ -0,0 +1,83 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.delete; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.http.HttpClient; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; +import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.DeleteSlotPacketParser; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +/** + * Created by steckbrief on 21.08.2016. + */ +public class DeleteTokenReceived implements OnIqPacketReceived { + private static final String HEADER_NAME_DELETE_TOKEN = "X-FILETRANSFER-HTTP-DELETE-TOKEN"; + private final DeleteRemoteFile remoteFile; + + public DeleteTokenReceived(DeleteRemoteFile remoteFile) { + this.remoteFile = remoteFile; + } + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + try { + String url = this.remoteFile.getPath(); + String deleteToken = DeleteSlotPacketParser.parseDeleteToken(packet); + Logging.d("filetransfer.http.delete", "Got delete token '" + deleteToken + "' for remote file '" + remoteFile.getPath() + "'"); + OkHttpClient client = HttpClient.getClient(true); + Request request = new Request.Builder() + .url(url) + .addHeader(HEADER_NAME_DELETE_TOKEN, deleteToken) + .delete() + .build(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + Logging.e("filetransfer.http.delete", "Error while connecting to '" + call.request().url() + "': " + e.getMessage()); + MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (response.isSuccessful()) { + MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETED); + Logging.i("filetransfer.http.delete", "Remote file successfully deleted '" + remoteFile.getPath() + "'"); + } else { + String detailedMessage = response.body().string(); + switch (response.code()) { + case 403: + case 404: + case 500: + try { + JSONObject jsonObject = new JSONObject(detailedMessage); + detailedMessage = jsonObject.getString("msg"); + } catch (JSONException e) { + Logging.e("filetransfer.http.delete", "Failed to get error message from expected json response: " + detailedMessage); + } + break; + } + Logging.e("filetransfer.http.delete", "Could not delete remote file '" + remoteFile.getPath() + "'. Response Code: " + response.code() + ", details: " + detailedMessage); + MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); + } + } + }); + + } catch (XmppException e) { + Logging.e("filetransfer.http.delete", "Error while trying to get the delete token: " + e.getMessage()); + MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java new file mode 100644 index 00000000..fbe7186b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java @@ -0,0 +1,87 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.upload; + +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.filetransfer.http.upload.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/http/upload/HttpFileUploader.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java new file mode 100644 index 00000000..318a8b8f --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java @@ -0,0 +1,149 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.upload; + +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.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.StreamUtil; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.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/http/upload/HttpUploadFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java new file mode 100644 index 00000000..f08e27f5 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java @@ -0,0 +1,91 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.upload; + +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.filetransfer.http.upload.HttpUpload; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.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 true if the file transfer was started successfully, false 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 true if the file transfer was started successfully, false 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 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 true if the message can be processed, false 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/http/upload/HttpUploadSlotRequestReceived.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadSlotRequestReceived.java new file mode 100644 index 00000000..2687878d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadSlotRequestReceived.java @@ -0,0 +1,35 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.upload; + +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.filetransfer.http.upload.HttpUploadSlot; +import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.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/http/upload/HttpUploadedFileEncryptionUiCallback.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadedFileEncryptionUiCallback.java new file mode 100644 index 00000000..f363a675 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadedFileEncryptionUiCallback.java @@ -0,0 +1,34 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.upload; + +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 { + 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/httpupload/HttpFileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java deleted file mode 100644 index f6596b0c..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileTransferEntity.java +++ /dev/null @@ -1,87 +0,0 @@ -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.filetransfer.http.upload.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 deleted file mode 100644 index 705dd23c..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpFileUploader.java +++ /dev/null @@ -1,149 +0,0 @@ -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.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.StreamUtil; -import de.thedevstack.conversationsplus.utils.UiUpdateHelper; -import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.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 deleted file mode 100644 index 6fd458a9..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadFileTransferService.java +++ /dev/null @@ -1,91 +0,0 @@ -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.filetransfer.http.upload.HttpUpload; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.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 true if the file transfer was started successfully, false 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 true if the file transfer was started successfully, false 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 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 true if the message can be processed, false 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 deleted file mode 100644 index 75e793f1..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadSlotRequestReceived.java +++ /dev/null @@ -1,35 +0,0 @@ -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.filetransfer.http.upload.HttpUploadSlot; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.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 deleted file mode 100644 index e3935252..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/httpupload/HttpUploadedFileEncryptionUiCallback.java +++ /dev/null @@ -1,34 +0,0 @@ -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 { - 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/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java index 934f16b6..c3d7c962 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java @@ -45,7 +45,7 @@ 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.services.filetransfer.http.DeleteRemoteFileService; +import de.thedevstack.conversationsplus.services.filetransfer.http.delete.DeleteRemoteFileService; import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; -- cgit v1.2.3 From ad096f4bfd220a106aab670105275397bfcbfd0e Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 23 Aug 2016 11:23:40 +0200 Subject: Confirmation dialog added before sending delete command for remote file --- .../http/delete/DeleteRemoteFileService.java | 33 +++++++++++++++----- .../conversationsplus/ui/ConversationFragment.java | 5 ++- .../ui/dialogs/SimpleConfirmDialog.java | 36 ++++++++++++++++++++++ .../ui/dialogs/UserDecisionDialog.java | 18 +++++------ .../ui/listeners/SimpleUserDecisionCallback.java | 9 ++++++ .../ui/listeners/UserDecisionListener.java | 6 ++-- 6 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/dialogs/SimpleConfirmDialog.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/listeners/SimpleUserDecisionCallback.java diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java index 42410ed4..2b26fd85 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java @@ -2,6 +2,7 @@ package de.thedevstack.conversationsplus.services.filetransfer.http.delete; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.ui.listeners.SimpleUserDecisionCallback; import de.thedevstack.conversationsplus.utils.XmppSendUtil; import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp; import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.FileTransferHttpDeleteSlotRequestPacketGenerator; @@ -11,20 +12,36 @@ import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; /** * Created by steckbrief on 21.08.2016. */ -public class DeleteRemoteFileService { - public void deleteRemoteFile(Message message) { - if (message.isHttpUploaded()) { - String path = message.getBody(); - if (message.hasFileOnRemoteHost()) { - path = message.getFileParams().url.toString(); +public class DeleteRemoteFileService implements SimpleUserDecisionCallback { + private Message message; + + public DeleteRemoteFileService(Message message) { + this.message = message; + } + + public void deleteRemoteFile() { + if (this.message.isHttpUploaded()) { + String path = this.message.getBody(); + if (this.message.hasFileOnRemoteHost()) { + path = this.message.getFileParams().url.toString(); } - DeleteRemoteFile remoteFile = new DeleteRemoteFile(path, message); - Account account = message.getConversation().getAccount(); + DeleteRemoteFile remoteFile = new DeleteRemoteFile(path, this.message); + Account account = this.message.getConversation().getAccount(); Jid host = account.getXmppConnection().findDiscoItemByFeature(FileTransferHttp.NAMESPACE); IqPacket request = FileTransferHttpDeleteSlotRequestPacketGenerator.generate(host, path); XmppSendUtil.sendIqPacket(account, request, new DeleteTokenReceived(remoteFile)); } } + + @Override + public void onYes() { + this.deleteRemoteFile(); + } + + @Override + public void onNo() { + // Nothing to do + } } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java index c3d7c962..397de0b3 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 de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.http.HttpDownloadConnection; import de.thedevstack.conversationsplus.services.filetransfer.http.delete.DeleteRemoteFileService; +import de.thedevstack.conversationsplus.ui.dialogs.SimpleConfirmDialog; import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; @@ -67,6 +68,8 @@ import de.thedevstack.conversationsplus.ui.adapter.MessageAdapter; import de.thedevstack.conversationsplus.ui.adapter.MessageAdapter.OnContactPictureClicked; import de.thedevstack.conversationsplus.ui.adapter.MessageAdapter.OnContactPictureLongClicked; import de.thedevstack.conversationsplus.ui.listeners.ConversationSwipeRefreshListener; +import de.thedevstack.conversationsplus.ui.listeners.SimpleUserDecisionCallback; +import de.thedevstack.conversationsplus.ui.listeners.UserDecisionListener; import de.thedevstack.conversationsplus.utils.GeoHelper; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.UIHelper; @@ -585,7 +588,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.msg_ctx_menu_delete_remote_file: - new DeleteRemoteFileService().deleteRemoteFile(selectedMessage); + new SimpleConfirmDialog(getActivity(), "Are you sure?", new DeleteRemoteFileService(selectedMessage)).show(); return true; case R.id.msg_ctx_mnu_details: new MessageDetailsDialog(getActivity(), selectedMessage).show(); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/SimpleConfirmDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/SimpleConfirmDialog.java new file mode 100644 index 00000000..04e60caa --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/SimpleConfirmDialog.java @@ -0,0 +1,36 @@ +package de.thedevstack.conversationsplus.ui.dialogs; + +import android.content.Context; +import android.content.DialogInterface; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.ui.listeners.SimpleUserDecisionCallback; + +/** + * A dialog to give the user the choice to decide whether to do something or not. + * A UserDecisionListener is used to provide the functionality to be performed by clicking on yes, or no. + */ +public class SimpleConfirmDialog extends AbstractAlertDialog { + protected final SimpleUserDecisionCallback callback; + + public SimpleConfirmDialog(Context context, String title, SimpleUserDecisionCallback userDecisionCallback) { + super(context, title); + this.callback = userDecisionCallback; + this.setPositiveButton(R.string.cplus_ok, new ConfirmOnClickListener()); + this.setNegativeButton(R.string.cancel, null); + } + + public SimpleConfirmDialog(Context context, int titleTextId, SimpleUserDecisionCallback userDecisionCallback) { + super(context, titleTextId); + this.callback = userDecisionCallback; + this.setPositiveButton(R.string.cplus_ok, new ConfirmOnClickListener()); + this.setNegativeButton(R.string.cancel, null); + } + + private class ConfirmOnClickListener implements DialogInterface.OnClickListener { + @Override + public void onClick(DialogInterface dialog, int which) { + callback.onYes(); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java index 25349b2d..e0834390 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java @@ -15,13 +15,11 @@ import de.thedevstack.conversationsplus.R; * The user also has the choice to save his answer for the future. * A UserDecisionListener is used to provide the functionality to be performed by clicking on yes, or no. */ -public class UserDecisionDialog extends AbstractAlertDialog { - protected final UserDecisionListener listener; +public class UserDecisionDialog extends SimpleConfirmDialog { protected final CheckBox rememberCheckBox; public UserDecisionDialog(Activity context, int questionResourceId, UserDecisionListener userDecisionListener) { - super(context, questionResourceId); - this.listener = userDecisionListener; + super(context, questionResourceId, userDecisionListener); int viewId = R.layout.dialog_userdecision; View view = context.getLayoutInflater().inflate(viewId, null); @@ -36,10 +34,10 @@ public class UserDecisionDialog extends AbstractAlertDialog { public void decide(UserDecision baseDecision) { switch (baseDecision) { case ALWAYS: - this.listener.onYes(); + this.callback.onYes(); break; case NEVER: - this.listener.onNo(); + this.callback.onNo(); break; case ASK: this.show(); @@ -51,9 +49,9 @@ public class UserDecisionDialog extends AbstractAlertDialog { @Override public void onClick(DialogInterface dialog, int which) { - listener.onYes(); + callback.onYes(); if (rememberCheckBox.isChecked()) { - listener.onRemember(UserDecision.ALWAYS); + ((UserDecisionListener)callback).onRemember(UserDecision.ALWAYS); } } } @@ -62,9 +60,9 @@ public class UserDecisionDialog extends AbstractAlertDialog { @Override public void onClick(DialogInterface dialog, int which) { - listener.onNo(); + callback.onNo(); if (rememberCheckBox.isChecked()) { - listener.onRemember(UserDecision.NEVER); + ((UserDecisionListener)callback).onRemember(UserDecision.NEVER); } } } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/SimpleUserDecisionCallback.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/SimpleUserDecisionCallback.java new file mode 100644 index 00000000..3ed49c2d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/SimpleUserDecisionCallback.java @@ -0,0 +1,9 @@ +package de.thedevstack.conversationsplus.ui.listeners; + +/** + * Callback to be executed on a user decision. + */ +public interface SimpleUserDecisionCallback { + void onYes(); + void onNo(); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java index fbee6290..45576225 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java @@ -3,10 +3,8 @@ package de.thedevstack.conversationsplus.ui.listeners; import de.thedevstack.conversationsplus.enums.UserDecision; /** - * Created by tzur on 31.10.2015. + * Callback to be executed on a user decision with the possibility to remember the decision. */ -public interface UserDecisionListener { - void onYes(); - void onNo(); +public interface UserDecisionListener extends SimpleUserDecisionCallback { void onRemember(UserDecision decision); } -- cgit v1.2.3 From 14e28d1e9afc058526eba4a6096d582f4810ea84 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 23 Aug 2016 11:34:14 +0200 Subject: Fixes FS#229: prompt for confirmation before deleting a file --- .../conversationsplus/ui/ConversationFragment.java | 12 ++------ .../ui/listeners/DeleteFileCallback.java | 36 ++++++++++++++++++++++ src/main/res/values-de/strings.xml | 1 + src/main/res/values/strings.xml | 1 + 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java index 397de0b3..56065ea4 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java @@ -68,6 +68,7 @@ import de.thedevstack.conversationsplus.ui.adapter.MessageAdapter; import de.thedevstack.conversationsplus.ui.adapter.MessageAdapter.OnContactPictureClicked; import de.thedevstack.conversationsplus.ui.adapter.MessageAdapter.OnContactPictureLongClicked; import de.thedevstack.conversationsplus.ui.listeners.ConversationSwipeRefreshListener; +import de.thedevstack.conversationsplus.ui.listeners.DeleteFileCallback; import de.thedevstack.conversationsplus.ui.listeners.SimpleUserDecisionCallback; import de.thedevstack.conversationsplus.ui.listeners.UserDecisionListener; import de.thedevstack.conversationsplus.utils.GeoHelper; @@ -588,7 +589,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.msg_ctx_menu_delete_remote_file: - new SimpleConfirmDialog(getActivity(), "Are you sure?", new DeleteRemoteFileService(selectedMessage)).show(); + new SimpleConfirmDialog(getActivity(), R.string.cplus_are_you_sure, new DeleteRemoteFileService(selectedMessage)).show(); return true; case R.id.msg_ctx_mnu_details: new MessageDetailsDialog(getActivity(), selectedMessage).show(); @@ -615,7 +616,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa retryDecryption(selectedMessage); return true; case R.id.delete_file: - deleteFile(selectedMessage); + new SimpleConfirmDialog(getActivity(), R.string.cplus_are_you_sure, new DeleteFileCallback(selectedMessage)).show(); return true; default: return super.onContextItemSelected(item); @@ -654,13 +655,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } } - private void deleteFile(Message message) { - if (FileBackend.deleteFile(message, activity.xmppConnectionService)) { - message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); - activity.xmppConnectionService.updateConversationUi(); - } - } - private void resendMessage(Message message) { if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { DownloadableFile file = FileBackend.getFile(message); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java new file mode 100644 index 00000000..3ff7f367 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java @@ -0,0 +1,36 @@ +package de.thedevstack.conversationsplus.ui.listeners; + +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.Transferable; +import de.thedevstack.conversationsplus.entities.TransferablePlaceholder; +import de.thedevstack.conversationsplus.persistance.FileBackend; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; + +/** + * Created by steckbrief on 23.08.2016. + */ +public class DeleteFileCallback implements SimpleUserDecisionCallback { + private final Message message; + + public DeleteFileCallback(Message message) { + this.message = message; + } + + private void deleteFile() { + if (FileBackend.deleteFile(this.message, XmppConnectionServiceAccessor.xmppConnectionService)) { + this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); + UiUpdateHelper.updateConversationUi(); + } + } + + @Override + public void onYes() { + this.deleteFile(); + } + + @Override + public void onNo() { + // Nothing to do + } +} diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 0e3b388d..4cfbd67a 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -634,4 +634,5 @@ Du vertraust diesem Kontakt bereits. Durch Auswählen von \"Fertig\" bestätigst du, dass %s Teil dieser Konferenz ist. Bild auswählen und zuschneiden Du hast diesen Account deaktiviert + Bist du dir sicher? diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 22b6c490..8136a84f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -675,4 +675,5 @@ No application found to share URI Share URI with… Delete remote file + Are you sure? -- cgit v1.2.3 From c6ed89e4109a3db25d9cae8ad1426a3294e721c3 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 23 Aug 2016 12:00:50 +0200 Subject: Javadoc added and access to xmppConnectionService moved --- .../conversationsplus/persistance/FileBackend.java | 5 +++-- .../conversationsplus/ui/listeners/DeleteFileCallback.java | 14 +++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java index 9f7e473e..e5597c3c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java @@ -28,6 +28,7 @@ import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; public class FileBackend { private static final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); @@ -90,10 +91,10 @@ public class FileBackend { } } - public static boolean deleteFile(Message message, XmppConnectionService xmppConnectionService) { + public static boolean deleteFile(Message message) { File file = getFile(message); if (file.delete()) { - updateMediaScanner(file, xmppConnectionService); + updateMediaScanner(file, XmppConnectionServiceAccessor.xmppConnectionService); return true; } else { return false; diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java index 3ff7f367..f855a90d 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java @@ -5,10 +5,9 @@ import de.thedevstack.conversationsplus.entities.Transferable; import de.thedevstack.conversationsplus.entities.TransferablePlaceholder; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.utils.UiUpdateHelper; -import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; /** - * Created by steckbrief on 23.08.2016. + * Callback for the user decision if a file should be deleted or not. */ public class DeleteFileCallback implements SimpleUserDecisionCallback { private final Message message; @@ -17,18 +16,27 @@ public class DeleteFileCallback implements SimpleUserDecisionCallback { this.message = message; } + /** + * Deletes the file and updates the UI. + */ private void deleteFile() { - if (FileBackend.deleteFile(this.message, XmppConnectionServiceAccessor.xmppConnectionService)) { + if (FileBackend.deleteFile(this.message)) { this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); UiUpdateHelper.updateConversationUi(); } } + /** + * Deletes the file. + */ @Override public void onYes() { this.deleteFile(); } + /** + * Nothing to do in this case. + */ @Override public void onNo() { // Nothing to do -- cgit v1.2.3 From 0ef9af997c98d0252512cc53f436709e88a1e025 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Tue, 23 Aug 2016 12:17:58 +0200 Subject: Fixes conditions for cplus db upgrades --- .../de/thedevstack/conversationsplus/persistance/DatabaseBackend.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java index 6442e909..7c2583ae 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java @@ -198,13 +198,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { protected void onUpgradeCPlusDatabase(SQLiteDatabase db, int oldVersion, int newVersion) { Logging.d("db.upgrade.cplus", "Updating Conversations+ database from version '" + oldVersion + "' to '" + newVersion + "'"); if (oldVersion < newVersion) { - if (oldVersion == 0 && newVersion == 1) { + if (oldVersion < 1 && newVersion >= 1) { Logging.d("db.upgrade.cplus", "Creating additional parameters table for messages."); db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE_V0); db.execSQL("INSERT INTO " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + "(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") " + " SELECT " + Message.UUID + " FROM " + Message.TABLENAME); } - if (newVersion == 2) { + if (oldVersion < 2 && newVersion >= 2) { Logging.d("db.upgrade.cplus", "Encrypt all passwords for the first time"); Cursor cursor = db.rawQuery("SELECT " + Account.UUID + ", " + Account.PASSWORD + " FROM " + Account.TABLENAME, new String[0]); while (cursor.moveToNext()) { -- cgit v1.2.3 From 297bed106efdfa619d700ae44ce047d7f2663c93 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Fri, 26 Aug 2016 10:52:07 +0200 Subject: Fixes FS#230: Uncaught NullPointerException in DNSHelper --- .../conversationsplus/utils/DNSHelper.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/DNSHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/DNSHelper.java index 90618292..a0fd9995 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/DNSHelper.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/DNSHelper.java @@ -135,15 +135,20 @@ public class DNSHelper { Logging.d("dns", "using dns server: " + dnsServerHostAddress + " to look up " + qname); try { DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServerHostAddress); - Record[] rrset = message.getAnswers(); - for (Record rr : rrset) { - Data d = rr.getPayload(); - if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) { - SRV srv = (SRV) d; - SrvRecord srvRecord = new SrvRecord(srv.getPriority(), srv.getName(), srv.getPort(), tlsSrvRecord); - result.add(srvRecord); + if (null != message) { + Record[] rrset = message.getAnswers(); + for (Record rr : rrset) { + Data d = rr.getPayload(); + if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) { + SRV srv = (SRV) d; + SrvRecord srvRecord = new SrvRecord(srv.getPriority(), srv.getName(), srv.getPort(), tlsSrvRecord); + result.add(srvRecord); + } } + } else { + Logging.e("dns", "No valid DNS message retrieved."); } + } catch (IOException e) { Logging.d("dns", "Error while retrieving SRV record '" + qname + "' for '" + host + "' from DNS '" + dnsServerHostAddress + "': " + e.getMessage()); } -- cgit v1.2.3 From 34fcdda53fa8ae1174909b62860534d2d874eb72 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Thu, 29 Sep 2016 11:57:16 +0200 Subject: Implements FS#235: Deletion of remote files uploaded via httpupload --- .../conversationsplus/crypto/PgpEngine.java | 11 +- .../crypto/axolotl/AxolotlServiceImpl.java | 7 +- .../conversationsplus/dto/RemoteFile.java | 37 ++++ .../conversationsplus/entities/Conversation.java | 7 +- .../conversationsplus/entities/FileParams.java | 129 +++++++++++ .../conversationsplus/entities/Message.java | 170 +++----------- .../conversationsplus/entities/RemoteFile.java | 37 ---- .../conversationsplus/enums/FileStatus.java | 17 ++ .../generator/MessageGenerator.java | 24 +- .../http/HttpConnectionManager.java | 5 - .../http/HttpDownloadConnection.java | 10 +- .../http/HttpUploadConnection.java | 243 --------------------- .../conversationsplus/parser/MessageParser.java | 7 +- .../persistance/CursorHelper.java | 203 ----------------- .../persistance/DatabaseBackend.java | 33 ++- .../conversationsplus/persistance/FileBackend.java | 81 ++++--- .../persistance/MessageDatabaseAccess.java | 59 ----- .../db/access/AbstractDatabaseAccess.java | 21 ++ .../persistance/db/access/CursorHelper.java | 203 +++++++++++++++++ .../db/access/MessageDatabaseAccess.java | 180 +++++++++++++++ .../FileParamsBodyToDatabaseFieldsMigration.java | 101 +++++++++ .../services/ExportLogsService.java | 2 +- .../services/NotificationService.java | 3 +- .../filetransfer/http/delete/DeleteRemoteFile.java | 4 +- .../http/delete/DeleteRemoteFileService.java | 11 +- .../http/delete/DeleteTokenReceived.java | 18 +- .../http/upload/HttpFileTransferEntity.java | 11 +- .../filetransfer/http/upload/HttpFileUploader.java | 1 + .../http/upload/HttpUploadFileTransferService.java | 3 +- .../conversationsplus/ui/ConversationFragment.java | 4 +- .../ui/adapter/ConversationAdapter.java | 2 +- .../ui/adapter/MessageAdapter.java | 65 ++++-- .../ui/dialogs/MessageDetailsDialog.java | 7 +- .../ResizePictureUserDecisionListener.java | 24 +- .../conversationsplus/utils/ConversationUtil.java | 7 +- .../conversationsplus/utils/FileUtils.java | 22 ++ .../conversationsplus/utils/ImageUtil.java | 22 +- .../conversationsplus/utils/MessageUtil.java | 61 +++--- src/main/res/layout/message_sent.xml | 12 + src/main/res/values/strings.xml | 3 + 40 files changed, 1013 insertions(+), 854 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/entities/RemoteFile.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java delete mode 100644 src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/persistance/db/access/AbstractDatabaseAccess.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/persistance/db/access/CursorHelper.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/FileParamsBodyToDatabaseFieldsMigration.java diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java index d0b30d6d..c97b8395 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java @@ -14,10 +14,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.URL; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; -import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.StreamUtil; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Account; @@ -106,8 +104,6 @@ public class PgpEngine { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: - URL url = message.getFileParams().url; - MessageUtil.updateFileParams(message, url); message.setEncryption(Message.ENCRYPTION_DECRYPTED); PgpEngine.this.mXmppConnectionService .updateMessage(message); @@ -149,12 +145,7 @@ public class PgpEngine { if (!message.needsUploading()) { params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - String body; - if (message.hasFileOnRemoteHost()) { - body = message.getFileParams().url.toString(); - } else { - body = message.getBody(); - } + String body = message.getBody(); InputStream is = new ByteArrayInputStream(body.getBytes()); final OutputStream os = new ByteArrayOutputStream(); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { 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 5e302011..215e0995 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java @@ -925,12 +925,7 @@ public class AxolotlServiceImpl implements OnAdvancedStreamFeaturesLoaded, Axolo XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation()); if (axolotlMessage != null) { - final String content; - if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url.toString(); - } else { - content = message.getBody(); - } + final String content = message.getBody(); try { axolotlMessage.encrypt(content); } catch (CryptoFailedException e) { diff --git a/src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java new file mode 100644 index 00000000..58b64b28 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java @@ -0,0 +1,37 @@ +package de.thedevstack.conversationsplus.dto; + +import android.support.annotation.NonNull; + +import java.io.Serializable; + +/** + * Created by steckbrief on 22.08.2016. + */ +public class RemoteFile implements Serializable { + private static final long serialVersionUID = 34564871234564L; + private final String path; + + public RemoteFile(@NonNull String path) { + this.path = path; + } + + public String getPath() { + return path; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RemoteFile that = (RemoteFile) o; + + return path.equals(that.path); + + } + + @Override + public int hashCode() { + return path.hashCode(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java index bfd00b5d..c0ae56fd 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java @@ -747,12 +747,7 @@ public class Conversation extends AbstractEntity implements Blockable { for (int i = this.messages.size() - 1; i >= 0; --i) { Message message = this.messages.get(i); if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { - String otherBody; - if (message.hasFileOnRemoteHost()) { - otherBody = message.getFileParams().url.toString(); - } else { - otherBody = message.getBody(); - } + String otherBody = message.getBody(); if (otherBody != null && otherBody.equals(body)) { return message; } diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java b/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java new file mode 100644 index 00000000..bce8e571 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java @@ -0,0 +1,129 @@ +package de.thedevstack.conversationsplus.entities; + +import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.utils.MimeUtils; + +/** + * + */ +public class FileParams { + private String name; + private String path; + private String url; + private String mimeType; + private long size = 0; + private int width = 0; + private int height = 0; + private FileStatus fileStatus; + + public FileParams() { + fileStatus = FileStatus.UNDEFINED; + } + + public FileParams(String url) { + this(); + this.url = url; + } + + public FileParams(String url, long size) { + this(url); + this.size = size; + } + + public FileParams(String url, long size, int width, int height) { + this(url, size); + this.width = width; + this.height = height; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPath() { + return path; + } + + /** + * Sets the path to the file. + * If no file name is stored yet here - this method tries to extract the file name from the path. + * If the file name is stored here and the path does not end with the name - the name is appended to the path. + * @param path the path to be stored + */ + public void setPath(String path) { + if (null != path) { + if (null != this.name) { + path = (!path.endsWith(this.name)) ? path + "/" + this.name : path; + } else { + if (!path.endsWith("/")) { + this.setName(path.substring(path.lastIndexOf('/') + 1)); + } + } + if (null == this.mimeType) { + int start = path.lastIndexOf('.') + 1; + if (start < path.length()) { + String extension = path.substring(start); + this.mimeType = MimeUtils.guessMimeTypeFromExtension(extension); + } + } + } + + this.path = path; + } + + public boolean isRemoteAvailable() { + return null != this.url || FileStatus.UPLOADED == this.fileStatus || FileStatus.DELETE_FAILED == this.fileStatus; + } + + public void setFileStatus(FileStatus fileStatus) { + this.fileStatus = fileStatus; + } + + public FileStatus getFileStatus() { + return this.fileStatus; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java index f081e82a..a81ba404 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java @@ -7,6 +7,7 @@ import java.net.MalformedURLException; import java.net.URL; import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlSession; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.utils.FileUtils; import de.thedevstack.conversationsplus.utils.MimeUtils; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; @@ -27,9 +28,6 @@ public class Message extends AbstractEntity { public static final int STATUS_SEND_RECEIVED = 7; public static final int STATUS_SEND_DISPLAYED = 8; - public static final int STATUS_REMOTE_FILE_DELETE_FAILED = 101; - public static final int STATUS_REMOTE_FILE_DELETED = 102; - public static final int ENCRYPTION_NONE = 0; public static final int ENCRYPTION_PGP = 1; public static final int ENCRYPTION_OTR = 2; @@ -87,6 +85,7 @@ public class Message extends AbstractEntity { private Decision mTreatAsDownloadAble = Decision.NOT_DECIDED; private boolean httpUploaded; + private FileParams fileParams; private Message() { @@ -338,6 +337,9 @@ public class Message extends AbstractEntity { public void setType(int type) { this.type = type; + if (null != this.fileParams && (type == Message.TYPE_FILE || type == Message.TYPE_IMAGE)) { + this.setFileParams(new FileParams()); + } } public boolean isCarbon() { @@ -371,17 +373,8 @@ public class Message extends AbstractEntity { || message.getBody() == null || message.getCounterpart() == null) { return false; } else { - String body, otherBody; - if (this.hasFileOnRemoteHost()) { - body = this.getFileParams().url.toString(); - } else { - body = this.getBody(); - } - if (message.hasFileOnRemoteHost()) { - otherBody = message.getFileParams().url.toString(); - } else { - otherBody = message.getBody(); - } + String body = this.getBody(); + String otherBody = message.getBody(); if (message.getRemoteMsgId() != null && this.getRemoteMsgId() != null) { return (message.getRemoteMsgId().equals(this.getRemoteMsgId()) @@ -496,27 +489,10 @@ public class Message extends AbstractEntity { } private String extractRelevantExtension(String path) { - if (path == null || path.isEmpty()) { - return null; - } - - String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase(); - - final String lastPart = FileUtils.getLastExtension(filename); - - if (!lastPart.isEmpty()) { - // we want the real file extension, not the crypto one - final String secondToLastPart = FileUtils.getSecondToLastExtension(filename); - if (!secondToLastPart.isEmpty() && Transferable.VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { - return secondToLastPart; - } else { - return lastPart; - } - } - return null; + return FileUtils.getRelevantExtension(path); } - public String getMimeType() { + public String getMimeType() { // TODO: Move to fileparams if (relativeFilePath != null) { int start = relativeFilePath.lastIndexOf('.') + 1; if (start < relativeFilePath.length()) { @@ -595,103 +571,6 @@ public class Message extends AbstractEntity { } } - public FileParams getFileParams() { - FileParams params = getLegacyFileParams(); - if (params != null) { - return params; - } - params = new FileParams(); - if (this.transferable != null) { - params.size = this.transferable.getFileSize(); - } - if (this.getBody() == null) { - return params; - } - String parts[] = this.getBody().split("\\|"); - switch (parts.length) { - case 1: - try { - params.size = Long.parseLong(parts[0]); - } catch (NumberFormatException e) { - try { - params.url = new URL(parts[0]); - } catch (MalformedURLException e1) { - params.url = null; - } - } - break; - case 2: - case 4: - try { - params.url = new URL(parts[0]); - } catch (MalformedURLException e1) { - params.url = null; - } - try { - params.size = Long.parseLong(parts[1]); - } catch (NumberFormatException e) { - params.size = 0; - } - try { - params.width = Integer.parseInt(parts[2]); - } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { - params.width = 0; - } - try { - params.height = Integer.parseInt(parts[3]); - } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { - params.height = 0; - } - break; - case 3: - try { - params.size = Long.parseLong(parts[0]); - } catch (NumberFormatException e) { - params.size = 0; - } - try { - params.width = Integer.parseInt(parts[1]); - } catch (NumberFormatException e) { - params.width = 0; - } - try { - params.height = Integer.parseInt(parts[2]); - } catch (NumberFormatException e) { - params.height = 0; - } - break; - } - return params; - } - - public FileParams getLegacyFileParams() { - FileParams params = new FileParams(); - if (this.getBody() == null) { - return params; - } - String parts[] = this.getBody().split(","); - if (parts.length == 3) { - try { - params.size = Long.parseLong(parts[0]); - } catch (NumberFormatException e) { - return null; - } - try { - params.width = Integer.parseInt(parts[1]); - } catch (NumberFormatException e) { - return null; - } - try { - params.height = Integer.parseInt(parts[2]); - } catch (NumberFormatException e) { - return null; - } - return params; - } else { - return null; - } - } - public void untie() { this.mNextMessage = null; this.mPreviousMessage = null; @@ -701,19 +580,22 @@ public class Message extends AbstractEntity { return type == TYPE_FILE || type == TYPE_IMAGE; } - public boolean hasFileOnRemoteHost() { - return isFileOrImage() && getFileParams().url != null; - } + public boolean hasFileAttached() { + return isFileOrImage() || isHttpUploaded() || (null != fileParams && null != fileParams.getPath()); + } - public boolean needsUploading() { - return isFileOrImage() && getFileParams().url == null; + /* + @TODO better + */ + public boolean hasFileOnRemoteHost() { + return hasFileAttached() && null != getFileParams() && getFileParams().isRemoteAvailable(); } - public class FileParams { - public URL url; - public long size = 0; - public int width = 0; - public int height = 0; + /* + @TODO better + */ + public boolean needsUploading() { + return hasFileAttached() && getFileParams().getFileStatus() == FileStatus.NEEDS_UPLOAD; } public void setFingerprint(String fingerprint) { @@ -768,6 +650,14 @@ public class Message extends AbstractEntity { this.httpUploaded = httpUploaded; } + public FileParams getFileParams() { + return this.fileParams; + } + + public void setFileParams(FileParams params) { + this.fileParams = params; + } + private static int getCleanedEncryption(int encryption) { if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { return ENCRYPTION_PGP; diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/RemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/entities/RemoteFile.java deleted file mode 100644 index 76c1dbc9..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/entities/RemoteFile.java +++ /dev/null @@ -1,37 +0,0 @@ -package de.thedevstack.conversationsplus.entities; - -import android.support.annotation.NonNull; - -import java.io.Serializable; - -/** - * Created by steckbrief on 22.08.2016. - */ -public class RemoteFile implements Serializable { - private static final long serialVersionUID = 34564871234564L; - private final String path; - - public RemoteFile(@NonNull String path) { - this.path = path; - } - - public String getPath() { - return path; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - RemoteFile that = (RemoteFile) o; - - return path.equals(that.path); - - } - - @Override - public int hashCode() { - return path.hashCode(); - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java b/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java new file mode 100644 index 00000000..b6a4ef9a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java @@ -0,0 +1,17 @@ +package de.thedevstack.conversationsplus.enums; + +/** + * Created by steckbrief on 23.08.2016. + */ +public enum FileStatus { + NOT_DOWNLOADED, + DOWNLOADED, + DOWNLOAD_FAILED, + DELETED, + DELETE_FAILED, + UPLOADED, + NEEDS_UPLOAD, + UNDEFINED, + NEEDS_DOWNLOAD, + DELETING; +} diff --git a/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java b/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java index 5a1d6f3f..678f2c75 100644 --- a/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java +++ b/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java @@ -13,6 +13,7 @@ import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlMessage; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; @@ -70,13 +71,13 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public static void addXhtmlImImage(MessagePacket packet, Message.FileParams params) { + public static void addXhtmlImImage(MessagePacket packet, FileParams params) { Element html = packet.addChild("html", "http://jabber.org/protocol/xhtml-im"); Element body = html.addChild("body", "http://www.w3.org/1999/xhtml"); Element img = body.addChild("img"); - img.setAttribute("src", params.url.toString()); - img.setAttribute("height", params.height); - img.setAttribute("width", params.width); + img.setAttribute("src", params.getUrl()); + img.setAttribute("height", params.getHeight()); + img.setAttribute("width", params.getWidth()); } public static void addMessageHints(MessagePacket packet) { @@ -94,12 +95,7 @@ public class MessageGenerator extends AbstractGenerator { MessagePacket packet = preparePacket(message); addMessageHints(packet); try { - String content; - if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url.toString(); - } else { - content = message.getBody(); - } + String content = message.getBody(); packet.setBody(otrSession.transformSending(content)[0]); return packet; } catch (OtrException e) { @@ -111,14 +107,14 @@ public class MessageGenerator extends AbstractGenerator { MessagePacket packet = preparePacket(message); String content; if (message.hasFileOnRemoteHost()) { - Message.FileParams fileParams = message.getFileParams(); - content = fileParams.url.toString(); + FileParams fileParams = message.getFileParams(); + content = message.getBody(); if (message.isHttpUploaded()) { packet.addChild(new HttpUploadHint()); } packet.addChild("x","jabber:x:oob").addChild("url").setContent(content); - if (fileParams.width > 0 && fileParams.height > 0) { - addXhtmlImImage(packet,fileParams); + if (fileParams.getWidth() > 0 && fileParams.getHeight() > 0) { + addXhtmlImImage(packet, fileParams); } } else { content = message.getBody(); diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java index 529d12c4..686587c7 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java @@ -32,7 +32,6 @@ public class HttpConnectionManager extends AbstractConnectionManager { } private List downloadConnections = new CopyOnWriteArrayList<>(); - private List uploadConnections = new CopyOnWriteArrayList<>(); public static HttpDownloadConnection createNewDownloadConnection(Message message) { return createNewDownloadConnection(message, false); @@ -49,10 +48,6 @@ public class HttpConnectionManager extends AbstractConnectionManager { this.downloadConnections.remove(connection); } - public void finishUploadConnection(HttpUploadConnection httpUploadConnection) { - this.uploadConnections.remove(httpUploadConnection); - } - public static void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) { final X509TrustManager trustManager; final HostnameVerifier hostnameVerifier; diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java index 3facc14a..d9fc9584 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java @@ -18,6 +18,7 @@ import javax.net.ssl.SSLHandshakeException; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.exceptions.RemoteFileNotFoundException; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.StreamUtil; @@ -74,11 +75,7 @@ public class HttpDownloadConnection implements Transferable { this.message = message; this.message.setTransferable(this); try { - if (message.hasFileOnRemoteHost()) { - mUrl = message.getFileParams().url; - } else { - mUrl = new URL(message.getBody()); - } + mUrl = new URL(message.getFileParams().getUrl()); final String sUrlFilename = mUrl.getPath().substring(mUrl.getPath().lastIndexOf('/')).toLowerCase(); final String lastPart = FileUtils.getLastExtension(sUrlFilename); @@ -131,6 +128,7 @@ public class HttpDownloadConnection implements Transferable { private void finish() { FileBackend.updateMediaScanner(file, mXmppConnectionService); message.setTransferable(null); + MessageUtil.setAndSaveFileStatus(this.message, FileStatus.DOWNLOADED); mHttpConnectionManager.finishConnection(this); if (message.getEncryption() == Message.ENCRYPTION_PGP) { message.getConversation().getAccount().getPgpDecryptionService().add(message); @@ -176,7 +174,7 @@ public class HttpDownloadConnection implements Transferable { HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); return; } catch (RemoteFileNotFoundException e) { - message.setNoDownloadable(); + message.setNoDownloadable(); // TODO Set remote file status to not-available cancel(); return; } catch (IOException e) { diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java deleted file mode 100644 index 3c49d083..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java +++ /dev/null @@ -1,243 +0,0 @@ -package de.thedevstack.conversationsplus.http; - -import android.app.PendingIntent; -import android.os.PowerManager; -import android.util.Pair; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Scanner; - -import javax.net.ssl.HttpsURLConnection; - -import de.thedevstack.android.logcat.Logging; -import de.thedevstack.conversationsplus.ConversationsPlusApplication; -import de.thedevstack.conversationsplus.utils.MessageUtil; -import de.thedevstack.conversationsplus.utils.StreamUtil; -import de.thedevstack.conversationsplus.Config; -import de.thedevstack.conversationsplus.entities.Account; -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.services.AbstractConnectionManager; -import de.thedevstack.conversationsplus.services.XmppConnectionService; -import de.thedevstack.conversationsplus.ui.UiCallback; -import de.thedevstack.conversationsplus.utils.CryptoHelper; -import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; -import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; -import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUpload; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUploadRequestSlotPacketGenerator; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUploadSlot; -import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.SlotPacketParser; -import de.thedevstack.conversationsplus.xmpp.jid.Jid; -import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; - -@Deprecated -public class HttpUploadConnection implements Transferable { - - private HttpConnectionManager mHttpConnectionManager; - private XmppConnectionService mXmppConnectionService; - - private boolean canceled = false; - private boolean delayed = false; - private Account account; - private DownloadableFile file; - private Message message; - private String mime; - private URL mGetUrl; - private URL mPutUrl; - - private byte[] key = null; - - private long transmitted = 0; - - private InputStream mFileInputStream; - - public HttpUploadConnection(HttpConnectionManager httpConnectionManager) { - this.mHttpConnectionManager = httpConnectionManager; - this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService; - } - - @Override - public boolean start() { - return false; - } - - @Override - public int getStatus() { - return STATUS_UPLOADING; - } - - @Override - public long getFileSize() { - return file == null ? 0 : file.getExpectedSize(); - } - - @Override - public int getProgress() { - if (file == null) { - return 0; - } - return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); - } - - @Override - public void cancel() { - this.canceled = true; - } - - private void fail() { - mHttpConnectionManager.finishUploadConnection(this); - message.setTransferable(null); - MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED); - StreamUtil.close(mFileInputStream); - } - - public void init(Message message, boolean delay) { - this.message = message; - this.message.setHttpUploaded(true); - this.message.setNoDownloadable(); - this.account = message.getConversation().getAccount(); - this.file = FileBackend.getFile(message, false); - this.mime = this.file.getMimeType(); - this.delayed = delay; - 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.file.setKeyAndIv(this.key); - } - Pair pair; - try { - pair = AbstractConnectionManager.createInputStream(file, true); - } catch (FileNotFoundException e) { - fail(); - return; - } - this.file.setExpectedSize(pair.second); - this.mFileInputStream = pair.first; - 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) { - 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); - MessageUtil.markMessage(message, Message.STATUS_UNSEND); - } - - private class FileUploader implements Runnable { - - @Override - public void run() { - this.upload(); - } - - private void upload() { - OutputStream os = null; - InputStream errorStream = null; - HttpURLConnection connection = null; - PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_upload_"+message.getUuid()); - try { - wakeLock.acquire(); - Logging.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()); - connection = (HttpURLConnection) mPutUrl.openConnection(); - - if (connection instanceof HttpsURLConnection) { - mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true); - } - connection.setRequestMethod("PUT"); - connection.setFixedLengthStreamingMode((int) file.getExpectedSize()); - connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime); - connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion()); - connection.setDoOutput(true); - connection.connect(); - os = connection.getOutputStream(); - transmitted = 0; - int count = -1; - byte[] buffer = new byte[4096]; - while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) { - transmitted += count; - os.write(buffer, 0, count); - mXmppConnectionService.updateConversationUi(); - } - os.flush(); - os.close(); - mFileInputStream.close(); - int code = connection.getResponseCode(); - if (code == 200 || code == 201) { - Logging.d(Config.LOGTAG, "finished uploading file"); - if (key != null) { - mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key)); - } - MessageUtil.updateFileParams(message, mGetUrl); - FileBackend.updateMediaScanner(file, mXmppConnectionService); - message.setTransferable(null); - message.setCounterpart(message.getConversation().getJid().toBareJid()); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - mXmppConnectionService.getPgpEngine().encrypt(message, new UiCallback() { - @Override - public void success(Message message) { - mXmppConnectionService.resendMessage(message,delayed); - } - - @Override - public void error(int errorCode, Message object) { - fail(); - } - - @Override - public void userInputRequried(PendingIntent pi, Message object) { - fail(); - } - }); - } else { - mXmppConnectionService.resendMessage(message, delayed); - } - } else { - errorStream = connection.getErrorStream(); - Logging.e("httpupload", "file upload failed: http code (" + code + ") " + new Scanner(errorStream).useDelimiter("\\A").next()); - 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()); - fail(); - } finally { - StreamUtil.close(os); - StreamUtil.close(errorStream); - if (connection != null) { - connection.disconnect(); - } - wakeLock.release(); - } - } - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java index 2f1701df..b02677a8 100644 --- a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java @@ -3,6 +3,8 @@ package de.thedevstack.conversationsplus.parser; import android.util.Log; import android.util.Pair; +import de.thedevstack.conversationsplus.entities.FileParams; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.xmpp.httpuploadim.HttpUploadHint; import de.tzur.conversations.Settings; @@ -10,7 +12,6 @@ import de.tzur.conversations.Settings; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; -import java.net.URL; import java.util.ArrayList; import java.util.Set; @@ -472,6 +473,10 @@ public class MessageParser extends AbstractParser implements && message.treatAsDownloadable() != Message.Decision.NEVER && ConversationsPlusPreferences.autoAcceptFileSize() > 0 && (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())) { + FileParams fileParams = new FileParams(); + fileParams.setFileStatus(FileStatus.NEEDS_DOWNLOAD); + fileParams.setUrl(message.getBody()); + message.setFileParams(fileParams); HttpConnectionManager.createNewDownloadConnection(message); } else { if (query == null) { diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java b/src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java deleted file mode 100644 index 7e3fdab0..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java +++ /dev/null @@ -1,203 +0,0 @@ -package de.thedevstack.conversationsplus.persistance; - -import android.database.Cursor; - -/** - * Created by steckbrief on 15.04.2016. - */ -public abstract class CursorHelper { - - static double getDouble(Cursor cursor, String columnName) { - if (null == cursor) { - return Double.MIN_VALUE; - } - int columnIndex = getColumnIndex(cursor, columnName); - if (columnIndex < 0) { - return Double.MIN_VALUE; - } - return getDouble(cursor, columnIndex); - } - - static String getString(Cursor cursor, String columnName) { - if (null == cursor) { - return null; - } - int columnIndex = getColumnIndex(cursor, columnName); - if (columnIndex < 0) { - return null; - } - return getString(cursor, columnIndex); - } - - static float getFloat(Cursor cursor, String columnName) { - if (null == cursor) { - return Float.MIN_VALUE; - } - int columnIndex = getColumnIndex(cursor, columnName); - if (columnIndex < 0) { - return Float.MIN_VALUE; - } - return getFloat(cursor, columnIndex); - } - - static int getInt(Cursor cursor, String columnName) { - if (null == cursor) { - return Integer.MIN_VALUE; - } - int columnIndex = getColumnIndex(cursor, columnName); - if (columnIndex < 0) { - return Integer.MIN_VALUE; - } - return getInt(cursor, columnIndex); - } - - static long getLong(Cursor cursor, String columnName) { - if (null == cursor) { - return Long.MIN_VALUE; - } - int columnIndex = getColumnIndex(cursor, columnName); - if (columnIndex < 0) { - return Long.MIN_VALUE; - } - return getLong(cursor, columnIndex); - } - - static int getShort(Cursor cursor, String columnName) { - if (null == cursor) { - return Short.MIN_VALUE; - } - int columnIndex = getColumnIndex(cursor, columnName); - if (columnIndex < 0) { - return Short.MIN_VALUE; - } - return getShort(cursor, columnIndex); - } - - static byte[] getBlob(Cursor cursor, String columnName) { - if (null == cursor) { - return null; - } - int columnIndex = getColumnIndex(cursor, columnName); - if (columnIndex < 0) { - return null; - } - return getBlob(cursor, columnIndex); - } - - /** - * Returns the zero-based index for the given column name, or -1 if the column doesn't exist. - * If you expect the column to exist use {@link Cursor#getColumnIndexOrThrow(String)} instead, which - * will make the error more clear. - * - * @param columnName the name of the target column. - * @return the zero-based column index for the given column name, or -1 if - * the column name does not exist. - * @see Cursor#getColumnIndexOrThrow(String) - */ - static int getColumnIndex(Cursor cursor, String columnName) { - return cursor.getColumnIndex(columnName); - } - - /** - * Returns the value of the requested column as a byte array. - * - *

The result and whether this method throws an exception when the - * column value is null or the column type is not a blob type is - * implementation-defined. - * - * @param columnIndex the zero-based index of the target column. - * @return the value of that column as a byte array. - */ - static byte[] getBlob(Cursor cursor, int columnIndex) { - return cursor.getBlob(columnIndex); - } - - /** - * Returns the value of the requested column as a String. - * - *

The result and whether this method throws an exception when the - * column value is null or the column type is not a string type is - * implementation-defined. - * - * @param columnIndex the zero-based index of the target column. - * @return the value of that column as a String. - */ - static String getString(Cursor cursor, int columnIndex) { - return cursor.getString(columnIndex); - } - - /** - * Returns the value of the requested column as a short. - * - *

The result and whether this method throws an exception when the - * column value is null, the column type is not an integral type, or the - * integer value is outside the range [Short.MIN_VALUE, - * Short.MAX_VALUE] is implementation-defined. - * - * @param columnIndex the zero-based index of the target column. - * @return the value of that column as a short. - */ - static short getShort(Cursor cursor, int columnIndex) { - return cursor.getShort(columnIndex); - } - - /** - * Returns the value of the requested column as an int. - * - *

The result and whether this method throws an exception when the - * column value is null, the column type is not an integral type, or the - * integer value is outside the range [Integer.MIN_VALUE, - * Integer.MAX_VALUE] is implementation-defined. - * - * @param columnIndex the zero-based index of the target column. - * @return the value of that column as an int. - */ - static int getInt(Cursor cursor, int columnIndex) { - return cursor.getInt(columnIndex); - } - - /** - * Returns the value of the requested column as a long. - * - *

The result and whether this method throws an exception when the - * column value is null, the column type is not an integral type, or the - * integer value is outside the range [Long.MIN_VALUE, - * Long.MAX_VALUE] is implementation-defined. - * - * @param columnIndex the zero-based index of the target column. - * @return the value of that column as a long. - */ - static long getLong(Cursor cursor, int columnIndex) { - return cursor.getLong(columnIndex); - } - - /** - * Returns the value of the requested column as a float. - * - *

The result and whether this method throws an exception when the - * column value is null, the column type is not a floating-point type, or the - * floating-point value is not representable as a float value is - * implementation-defined. - * - * @param columnIndex the zero-based index of the target column. - * @return the value of that column as a float. - */ - static float getFloat(Cursor cursor, int columnIndex) { - return cursor.getFloat(columnIndex); - } - - /** - * Returns the value of the requested column as a double. - * - *

The result and whether this method throws an exception when the - * column value is null, the column type is not a floating-point type, or the - * floating-point value is not representable as a double value is - * implementation-defined. - * - * @param columnIndex the zero-based index of the target column. - * @return the value of that column as a double. - */ - static double getDouble(Cursor cursor, int columnIndex) { - return cursor.getDouble(columnIndex); - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java index 7c2583ae..e721afbb 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java @@ -35,6 +35,7 @@ import org.json.JSONException; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceImpl; import de.thedevstack.conversationsplus.crypto.axolotl.SQLiteAxolotlStore; import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlSession; @@ -44,6 +45,8 @@ import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Roster; import de.thedevstack.conversationsplus.entities.ServiceDiscoveryResult; +import de.thedevstack.conversationsplus.persistance.db.access.CursorHelper; +import de.thedevstack.conversationsplus.persistance.db.access.MessageDatabaseAccess; import de.thedevstack.conversationsplus.utils.SimpleCryptoUtil; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -55,7 +58,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; private static final int DATABASE_VERSION = 25; private static final int C_TO_CPLUS_VERSION_OFFSET = 1000; - private static final int CPLUS_DATABASE_VERSION = 2; + private static final int CPLUS_DATABASE_VERSION = 3; private static final int CPLUS_DATABASE_VERSION_MULTIPLIER = 100; private static final int PHYSICAL_DATABASE_VERSION = DATABASE_VERSION + C_TO_CPLUS_VERSION_OFFSET + (CPLUS_DATABASE_VERSION * CPLUS_DATABASE_VERSION_MULTIPLIER); @@ -191,18 +194,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); db.execSQL(CREATE_IDENTITIES_STATEMENT); - // Create Conversations+ related tables - db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE_V0); + MessageDatabaseAccess.create(db); } protected void onUpgradeCPlusDatabase(SQLiteDatabase db, int oldVersion, int newVersion) { Logging.d("db.upgrade.cplus", "Updating Conversations+ database from version '" + oldVersion + "' to '" + newVersion + "'"); if (oldVersion < newVersion) { if (oldVersion < 1 && newVersion >= 1) { - Logging.d("db.upgrade.cplus", "Creating additional parameters table for messages."); - db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE_V0); - db.execSQL("INSERT INTO " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + "(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") " - + " SELECT " + Message.UUID + " FROM " + Message.TABLENAME); + MessageDatabaseAccess.upgrade(db, oldVersion, newVersion); } if (oldVersion < 2 && newVersion >= 2) { Logging.d("db.upgrade.cplus", "Encrypt all passwords for the first time"); @@ -217,6 +216,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { } cursor.close(); } + + if (oldVersion < 3 && newVersion >= 3) { + MessageDatabaseAccess.upgrade(db, oldVersion, newVersion); + } } } @@ -434,6 +437,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } + public static synchronized DatabaseBackend getInstance() { + return getInstance(ConversationsPlusApplication.getAppContext()); + } + public static synchronized DatabaseBackend getInstance(Context context) { if (instance == null) { instance = new DatabaseBackend(context); @@ -657,15 +664,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public void updateMessage(Message message) { - Logging.d("db.msg.update", "Updating message with uuid '" + message.getUuid() + "', isRead: " + message.isRead()); - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {message.getUuid()}; - db.beginTransaction(); - db.update(Message.TABLENAME, message.getContentValues(), Message.UUID - + "=?", args); - db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, MessageDatabaseAccess.getAdditionalParametersContentValues(message), MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", args); - db.setTransactionSuccessful(); - db.endTransaction(); + this.updateMessage(message, message.getUuid()); } public void updateMessage(Message message, String uuid) { @@ -676,7 +675,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.beginTransaction(); db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args); - db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, MessageDatabaseAccess.getAdditionalParametersContentValues(message), MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", args); + MessageDatabaseAccess.updateMessageParameters(db, message, uuid); db.setTransactionSuccessful(); db.endTransaction(); } diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java index e5597c3c..a35ed043 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java @@ -20,6 +20,7 @@ import java.util.Locale; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.exceptions.FileCopyException; import de.thedevstack.conversationsplus.persistance.observers.FileDeletionObserver; import de.thedevstack.conversationsplus.utils.StreamUtil; @@ -106,31 +107,55 @@ public class FileBackend { } public static DownloadableFile getFile(Message message, boolean decrypted) { - final boolean encrypted = !decrypted - && (message.getEncryption() == Message.ENCRYPTION_PGP - || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); - final DownloadableFile file; - String path = message.getRelativeFilePath(); - if (path == null) { - path = message.getUuid(); - } - if (path.startsWith("/")) { - file = new DownloadableFile(path); - } else { - String mime = message.getMimeType(); - if (mime != null && mime.startsWith("image")) { - file = new DownloadableFile(getConversationsImageDirectory() + path); - } else { - file = new DownloadableFile(getConversationsFileDirectory() + path); - } - } - if (encrypted) { - return new DownloadableFile(getConversationsFileDirectory() + file.getName() + ".pgp"); - } else { - return file; - } + return new DownloadableFile(getFilePath(message, decrypted)); } + protected static String getFilePath(Message message, String extension) { + String path = FileBackend.getFilePath(message, true); + if (!path.endsWith(extension)) { + path += "." + extension; + } + + return path; + } + + protected static String getFilePath(Message message, Uri uri) { + String mime = ConversationsPlusApplication.getInstance().getContentResolver().getType(uri); + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); + + return getFilePath(message, extension); + } + + protected static String getFilePath(Message message, boolean decrypted) { + final boolean encrypted = !decrypted + && (message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); + FileParams fileParams = message.getFileParams(); + if (null == fileParams) { + fileParams = new FileParams(); + message.setFileParams(fileParams); + } + String path = fileParams.getPath(); + + if (null == path) { // File does not yet exist + path = message.getUuid(); + String mime = message.getMimeType(); + if (mime != null && mime.startsWith("image")) { // TODO: Check if this can be determined in a better way + path = getConversationsImageDirectory() + path; + } else { + path = getConversationsFileDirectory() + path; + } + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); + path += "." + extension; + + fileParams.setPath(path); + } + if (encrypted) { + path += ".pgp"; + } + return path; + } + public static String getConversationsFileDirectory() { return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + ConversationsPlusPreferences.fileTransferFolder() + File.separator; } @@ -175,14 +200,16 @@ public class FileBackend { public static void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); - String mime = ConversationsPlusApplication.getInstance().getContentResolver().getType(uri); - String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); - message.setRelativeFilePath(message.getUuid() + "." + extension); + String path = getFilePath(message, uri); + message.getFileParams().setPath(path); + message.setRelativeFilePath(path); // TODO: Remove copyFileToPrivateStorage(getFile(message), uri); } public static DownloadableFile compressImageAndCopyToPrivateStorage(Message message, Bitmap scaledBitmap) throws FileCopyException { - message.setRelativeFilePath(FileBackend.getPrivateImageDirectoryPath() + message.getUuid() + ".jpg"); + String path = getFilePath(message, "jpg"); + message.getFileParams().setPath(path); + message.setRelativeFilePath(path); // TODO: Remove DownloadableFile file = getFile(message); file.getParentFile().mkdirs(); OutputStream os = null; diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java b/src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java deleted file mode 100644 index 7776174d..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java +++ /dev/null @@ -1,59 +0,0 @@ -package de.thedevstack.conversationsplus.persistance; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - -import de.thedevstack.android.logcat.Logging; -import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; -import de.thedevstack.conversationsplus.xmpp.jid.Jid; - -/** - * Created by steckbrief on 15.04.2016. - */ -public class MessageDatabaseAccess { - static final String TABLE_NAME_ADDITIONAL_PARAMETERS = "message_parameters"; - static final String COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD = "httpupload"; - static final String COLUMN_NAME_MSG_PARAMS_MSGUUID = "message_uuid"; - static final String COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION = "treatasdownloadable_decision"; - - static final String TABLE_ADDITIONAL_PARAMETERS_CREATE_V0 = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " (" - + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, " - + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD + " INTEGER DEFAULT 0, " - + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', " - + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)"; - - static ContentValues getAdditionalParametersContentValues(Message message) { - ContentValues additionalParameters = new ContentValues(); - additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID, message.getUuid()); - additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD, message.isHttpUploaded() ? 1 : 0); - additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION, message.treatAsDownloadable().name()); - - return additionalParameters; - } - - static void populateMessageParametersFromCursor(Cursor cursor, Message message) { - boolean isHttpUploaded = CursorHelper.getInt(cursor, COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD) == 1; - message.setHttpUploaded(isHttpUploaded); - String downloadable = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION); - Message.Decision treatAsDownloadable = Message.Decision.NOT_DECIDED; - try { - treatAsDownloadable = Message.Decision.valueOf(downloadable); - } catch (IllegalArgumentException e) { - // Should only happen if the database is corrupted, but to be on the save side catch it here - Logging.e("db.msg", "Unknown Decision for treatAsDownloadable found: '" + downloadable + "'"); - } - message.setTreatAsDownloadable(treatAsDownloadable); - } - - static void populateMessageParameters(SQLiteDatabase db, Message message) { - Cursor paramsCursor = db.query(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, - null, MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", - new String[] {message.getUuid()}, - null, null, null, null); - paramsCursor.moveToNext(); - MessageDatabaseAccess.populateMessageParametersFromCursor(paramsCursor, message); - paramsCursor.close(); - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/AbstractDatabaseAccess.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/AbstractDatabaseAccess.java new file mode 100644 index 00000000..407859c7 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/AbstractDatabaseAccess.java @@ -0,0 +1,21 @@ +package de.thedevstack.conversationsplus.persistance.db.access; + +import android.database.sqlite.SQLiteDatabase; + +/** + * Created by steckbrief on 24.08.2016. + */ +public abstract class AbstractDatabaseAccess { + static void executeAlterStatements(SQLiteDatabase db, String... alterStatements) { + for (String alterStatement : alterStatements) { + db.execSQL(alterStatement); + } + } + + static void addNewColumns(SQLiteDatabase db, String tableName, String... columnDefinitions) { + for (String columnDefinition : columnDefinitions) { + String alterStatement = "ALTER TABLE " + tableName + " ADD COLUMN " + columnDefinition; + db.execSQL(alterStatement); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/CursorHelper.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/CursorHelper.java new file mode 100644 index 00000000..122e5160 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/CursorHelper.java @@ -0,0 +1,203 @@ +package de.thedevstack.conversationsplus.persistance.db.access; + +import android.database.Cursor; + +/** + * Created by steckbrief on 15.04.2016. + */ +public abstract class CursorHelper { + + public static double getDouble(Cursor cursor, String columnName) { + if (null == cursor) { + return Double.MIN_VALUE; + } + int columnIndex = getColumnIndex(cursor, columnName); + if (columnIndex < 0) { + return Double.MIN_VALUE; + } + return getDouble(cursor, columnIndex); + } + + public static String getString(Cursor cursor, String columnName) { + if (null == cursor) { + return null; + } + int columnIndex = getColumnIndex(cursor, columnName); + if (columnIndex < 0) { + return null; + } + return getString(cursor, columnIndex); + } + + public static float getFloat(Cursor cursor, String columnName) { + if (null == cursor) { + return Float.MIN_VALUE; + } + int columnIndex = getColumnIndex(cursor, columnName); + if (columnIndex < 0) { + return Float.MIN_VALUE; + } + return getFloat(cursor, columnIndex); + } + + public static int getInt(Cursor cursor, String columnName) { + if (null == cursor) { + return Integer.MIN_VALUE; + } + int columnIndex = getColumnIndex(cursor, columnName); + if (columnIndex < 0) { + return Integer.MIN_VALUE; + } + return getInt(cursor, columnIndex); + } + + public static long getLong(Cursor cursor, String columnName) { + if (null == cursor) { + return Long.MIN_VALUE; + } + int columnIndex = getColumnIndex(cursor, columnName); + if (columnIndex < 0) { + return Long.MIN_VALUE; + } + return getLong(cursor, columnIndex); + } + + public static int getShort(Cursor cursor, String columnName) { + if (null == cursor) { + return Short.MIN_VALUE; + } + int columnIndex = getColumnIndex(cursor, columnName); + if (columnIndex < 0) { + return Short.MIN_VALUE; + } + return getShort(cursor, columnIndex); + } + + public static byte[] getBlob(Cursor cursor, String columnName) { + if (null == cursor) { + return null; + } + int columnIndex = getColumnIndex(cursor, columnName); + if (columnIndex < 0) { + return null; + } + return getBlob(cursor, columnIndex); + } + + /** + * Returns the zero-based index for the given column name, or -1 if the column doesn't exist. + * If you expect the column to exist use {@link Cursor#getColumnIndexOrThrow(String)} instead, which + * will make the error more clear. + * + * @param columnName the name of the target column. + * @return the zero-based column index for the given column name, or -1 if + * the column name does not exist. + * @see Cursor#getColumnIndexOrThrow(String) + */ + public static int getColumnIndex(Cursor cursor, String columnName) { + return cursor.getColumnIndex(columnName); + } + + /** + * Returns the value of the requested column as a byte array. + * + *

The result and whether this method throws an exception when the + * column value is null or the column type is not a blob type is + * implementation-defined. + * + * @param columnIndex the zero-based index of the target column. + * @return the value of that column as a byte array. + */ + public static byte[] getBlob(Cursor cursor, int columnIndex) { + return cursor.getBlob(columnIndex); + } + + /** + * Returns the value of the requested column as a String. + * + *

The result and whether this method throws an exception when the + * column value is null or the column type is not a string type is + * implementation-defined. + * + * @param columnIndex the zero-based index of the target column. + * @return the value of that column as a String. + */ + public static String getString(Cursor cursor, int columnIndex) { + return cursor.getString(columnIndex); + } + + /** + * Returns the value of the requested column as a short. + * + *

The result and whether this method throws an exception when the + * column value is null, the column type is not an integral type, or the + * integer value is outside the range [Short.MIN_VALUE, + * Short.MAX_VALUE] is implementation-defined. + * + * @param columnIndex the zero-based index of the target column. + * @return the value of that column as a short. + */ + public static short getShort(Cursor cursor, int columnIndex) { + return cursor.getShort(columnIndex); + } + + /** + * Returns the value of the requested column as an int. + * + *

The result and whether this method throws an exception when the + * column value is null, the column type is not an integral type, or the + * integer value is outside the range [Integer.MIN_VALUE, + * Integer.MAX_VALUE] is implementation-defined. + * + * @param columnIndex the zero-based index of the target column. + * @return the value of that column as an int. + */ + public static int getInt(Cursor cursor, int columnIndex) { + return cursor.getInt(columnIndex); + } + + /** + * Returns the value of the requested column as a long. + * + *

The result and whether this method throws an exception when the + * column value is null, the column type is not an integral type, or the + * integer value is outside the range [Long.MIN_VALUE, + * Long.MAX_VALUE] is implementation-defined. + * + * @param columnIndex the zero-based index of the target column. + * @return the value of that column as a long. + */ + public static long getLong(Cursor cursor, int columnIndex) { + return cursor.getLong(columnIndex); + } + + /** + * Returns the value of the requested column as a float. + * + *

The result and whether this method throws an exception when the + * column value is null, the column type is not a floating-point type, or the + * floating-point value is not representable as a float value is + * implementation-defined. + * + * @param columnIndex the zero-based index of the target column. + * @return the value of that column as a float. + */ + public static float getFloat(Cursor cursor, int columnIndex) { + return cursor.getFloat(columnIndex); + } + + /** + * Returns the value of the requested column as a double. + * + *

The result and whether this method throws an exception when the + * column value is null, the column type is not a floating-point type, or the + * floating-point value is not representable as a double value is + * implementation-defined. + * + * @param columnIndex the zero-based index of the target column. + * @return the value of that column as a double. + */ + public static double getDouble(Cursor cursor, int columnIndex) { + return cursor.getDouble(columnIndex); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java new file mode 100644 index 00000000..51ad99a4 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java @@ -0,0 +1,180 @@ +package de.thedevstack.conversationsplus.persistance.db.access; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.entities.FileParams; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.persistance.db.migrations.FileParamsBodyToDatabaseFieldsMigration; + +/** + * + */ +public class MessageDatabaseAccess extends AbstractDatabaseAccess { + // since cplus db version 1 + public static final String TABLE_NAME_ADDITIONAL_PARAMETERS = "message_parameters"; + private static final String COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD = "httpupload"; + private static final String COLUMN_NAME_MSG_PARAMS_MSGUUID = "message_uuid"; + private static final String COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION = "treatasdownloadable_decision"; + // since cplus db version 3 + private static final String COLUMN_NAME_MSG_PARAMS_FILE_SIZE = "file_size"; + private static final String COLUMN_NAME_MSG_PARAMS_FILE_TYPE = "file_type"; + private static final String COLUMN_NAME_MSG_PARAMS_FILE_WIDTH = "file_width"; + private static final String COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT = "file_height"; + private static final String COLUMN_NAME_MSG_PARAMS_FILE_STATUS = "file_status"; + private static final String COLUMN_NAME_MSG_PARAMS_FILE_NAME = "file_name"; + private static final String COLUMN_NAME_MSG_PARAMS_FILE_PATH = "file_path"; + private static final String COLUMN_NAME_MSG_PARAMS_FILE_URL = "file_url"; + + private static final String TABLE_ADDITIONAL_PARAMETERS_CREATE_V1 = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " (" + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD + " INTEGER DEFAULT 0, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', " + + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)"; + + private static final String TABLE_ADDITIONAL_PARAMETERS_CREATE = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " (" + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD + " INTEGER DEFAULT 0, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_TYPE + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_SIZE + " INTEGER DEFAULT 0, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT + " INTEGER DEFAULT 0, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_WIDTH + " INTEGER DEFAULT 0, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_STATUS + " TEXT DEFAULT 'UNDEFINED', " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_NAME + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_PATH + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_URL + " TEXT DEFAULT NULL, " + + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)"; + + public static void updateMessageParameters(SQLiteDatabase db, Message message, String uuid) { + String[] args = {uuid}; + db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, MessageDatabaseAccess.getAdditionalParametersContentValues(message), MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", args); + } + + public static ContentValues getAdditionalParametersContentValues(Message message) { + ContentValues additionalParameters = new ContentValues(); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID, message.getUuid()); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD, message.isHttpUploaded() ? 1 : 0); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION, message.treatAsDownloadable().name()); + if (null != message.getFileParams()) { + FileParams fileParams = message.getFileParams(); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_TYPE, fileParams.getMimeType()); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_SIZE, fileParams.getSize()); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT, fileParams.getHeight()); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_WIDTH, fileParams.getWidth()); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_PATH, fileParams.getPath()); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_URL, fileParams.getUrl()); + if (null != fileParams.getFileStatus()) { + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_STATUS, fileParams.getFileStatus().name()); + } + } + + return additionalParameters; + } + + private static void populateMessageParametersFromCursor(Cursor cursor, Message message) { + boolean isHttpUploaded = CursorHelper.getInt(cursor, COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD) == 1; + message.setHttpUploaded(isHttpUploaded); + String downloadable = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION); + Message.Decision treatAsDownloadable = Message.Decision.NOT_DECIDED; + try { + treatAsDownloadable = Message.Decision.valueOf(downloadable); + } catch (IllegalArgumentException e) { + // Should only happen if the database is corrupted, but to be on the save side catch it here + Logging.e("db.msg", "Unknown Decision for treatAsDownloadable found: '" + downloadable + "'"); + } + + message.setTreatAsDownloadable(treatAsDownloadable); + + if (message.hasFileAttached()) { + FileParams fileParams = new FileParams(message.getBody()); + String fileType = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_TYPE); + fileParams.setMimeType(fileType); + String name = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_NAME); + fileParams.setName(name); + String path = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_PATH); + fileParams.setPath(path); + String url = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_URL); + fileParams.setUrl(url); + long fileSize = CursorHelper.getLong(cursor, COLUMN_NAME_MSG_PARAMS_FILE_SIZE); + fileParams.setSize(fileSize); + int imageHeight = CursorHelper.getInt(cursor, COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT); + fileParams.setHeight(imageHeight); + int imageWidth = CursorHelper.getInt(cursor, COLUMN_NAME_MSG_PARAMS_FILE_WIDTH); + fileParams.setWidth(imageWidth); + String status = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_STATUS); + if (null != status && !status.isEmpty()) { + FileStatus fileStatus = FileStatus.UNDEFINED; + try { + fileStatus = FileStatus.valueOf(status); + } catch (IllegalArgumentException e) { + // Should only happen if the database is corrupted, but to be on the save side catch it here + Logging.e("db.msg", "Unknown FileStatus for fileParams.fileStatus found: '" + status + "'"); + } + fileParams.setFileStatus(fileStatus); + } + message.setFileParams(fileParams); + } + } + + public static void populateMessageParameters(SQLiteDatabase db, Message message) { + Cursor paramsCursor = db.query(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, + null, MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", + new String[] {message.getUuid()}, + null, null, null, null); + paramsCursor.moveToNext(); + MessageDatabaseAccess.populateMessageParametersFromCursor(paramsCursor, message); + paramsCursor.close(); + } + + public static void upgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 1 && newVersion >= 1) { + Logging.d("db.upgrade.cplus", "Creating additional parameters table for messages."); + db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE_V1); + db.execSQL("INSERT INTO " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + "(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") " + + " SELECT " + Message.UUID + " FROM " + Message.TABLENAME); + } + if (oldVersion < 3 && newVersion >= 3) { + Logging.d("db.upgrade.cplus", "Upgrade " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS); + String[] columnDefinitions = { + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_TYPE + " TEXT DEFAULT NULL", + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_SIZE + " INTEGER DEFAULT 0", + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT + " INTEGER DEFAULT 0", + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_WIDTH + " INTEGER DEFAULT 0", + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_STATUS + " TEXT DEFAULT 'UNDEFINED'", + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_NAME + " TEXT DEFAULT NULL", + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_PATH + " TEXT DEFAULT NULL", + }; + + addNewColumns(db, MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, columnDefinitions); + + Logging.d("db.upgrade.cplus", "Migrate file params from message body to database fields"); + Cursor cursor = db.rawQuery("SELECT " + Message.UUID + ", " + Message.BODY + " FROM " + Message.TABLENAME + " WHERE " + Message.TYPE + "=? OR " + Message.TYPE + "=?", new String[] {String.valueOf(Message.TYPE_FILE), String.valueOf(Message.TYPE_IMAGE)}); + while (cursor.moveToNext()) { + String uuid = CursorHelper.getString(cursor, Message.UUID); + String body = CursorHelper.getString(cursor, Message.BODY); + FileParams fileParams = FileParamsBodyToDatabaseFieldsMigration.getFileParams(body); + String newBody = fileParams.getUrl(); + ContentValues values = new ContentValues(); + values.put(Message.BODY, newBody); + db.update(Message.TABLENAME, values, Message.UUID + "=?", new String[] {uuid}); + + ContentValues parameterValues = new ContentValues(); + parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_SIZE, fileParams.getSize()); + parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT, fileParams.getHeight()); + parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_WIDTH, fileParams.getWidth()); + + db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, parameterValues, MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", new String[] {uuid}); + } + cursor.close(); + } + } + + public static void create(SQLiteDatabase db) { + // Create Conversations+ related tables + db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/FileParamsBodyToDatabaseFieldsMigration.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/FileParamsBodyToDatabaseFieldsMigration.java new file mode 100644 index 00000000..c0aa63c0 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/FileParamsBodyToDatabaseFieldsMigration.java @@ -0,0 +1,101 @@ +package de.thedevstack.conversationsplus.persistance.db.migrations; + +import java.net.MalformedURLException; +import java.net.URL; + +import de.thedevstack.conversationsplus.entities.FileParams; + +/** + * Created by steckbrief on 24.08.2016. + */ +public class FileParamsBodyToDatabaseFieldsMigration { + + public static FileParams getFileParams(String body) { + FileParams params = getLegacyFileParams(body); + if (params != null) { + return params; + } + params = new FileParams(); + if (body == null) { + return params; + } + String parts[] = body.split("\\|"); + switch (parts.length) { + case 1: + try { + params.setSize(Long.parseLong(parts[0])); + } catch (NumberFormatException e) { + try { + URL url = new URL(parts[0]); + params.setUrl(url.toString()); + } catch (MalformedURLException e1) { + } + } + break; + case 2: + case 4: + try { + URL url = new URL(parts[0]); + params.setUrl(url.toString()); + } catch (MalformedURLException e1) { + } + try { + params.setSize(Long.parseLong(parts[1])); + } catch (NumberFormatException e) { + } + try { + params.setWidth(Integer.parseInt(parts[2])); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + } + try { + params.setHeight(Integer.parseInt(parts[3])); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + } + break; + case 3: + try { + params.setSize(Long.parseLong(parts[0])); + } catch (NumberFormatException e) { + } + try { + params.setWidth(Integer.parseInt(parts[1])); + } catch (NumberFormatException e) { + } + try { + params.setHeight(Integer.parseInt(parts[2])); + } catch (NumberFormatException e) { + } + break; + } + return params; + } + + private static FileParams getLegacyFileParams(String body) { + FileParams params = new FileParams(); + if (body == null) { + return params; + } + String parts[] = body.split(","); + if (parts.length == 3) { + try { + params.setSize(Long.parseLong(parts[0])); + } catch (NumberFormatException e) { + return null; + } + try { + params.setWidth(Integer.parseInt(parts[1])); + } catch (NumberFormatException e) { + return null; + } + try { + params.setHeight(Integer.parseInt(parts[2])); + } catch (NumberFormatException e) { + return null; + } + return params; + } else { + return null; + } + } + +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java b/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java index 18930d1c..0dcfa05e 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java @@ -103,7 +103,7 @@ public class ExportLogsService extends Service { break; } if (jid != null) { - String body = message.hasFileOnRemoteHost() ? message.getFileParams().url.toString() : message.getBody(); + String body = message.hasFileOnRemoteHost() ? message.getFileParams().getUrl() : message.getBody(); bw.write(String.format(MESSAGE_STRING_FORMAT, date, jid, body.replace("\\\n", "\\ \n").replace("\n", "\\ \n"))); } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java index a3142df4..f4388b21 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java @@ -356,7 +356,8 @@ public class NotificationService { if (message.getType() != Message.TYPE_TEXT && message.getTransferable() == null && message.getEncryption() != Message.ENCRYPTION_PGP - && message.getFileParams().height > 0) { + && message.getFileParams() != null + && message.getFileParams().getHeight() > 0) { // TODO Use FileParams.getMimeType() return message; } } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java index 39a369ef..1549388e 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java @@ -3,10 +3,10 @@ package de.thedevstack.conversationsplus.services.filetransfer.http.delete; import android.support.annotation.NonNull; import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.entities.RemoteFile; +import de.thedevstack.conversationsplus.dto.RemoteFile; /** - * Created by steckbrief on 22.08.2016. + * */ public class DeleteRemoteFile extends RemoteFile { private final Message message; diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java index 2b26fd85..eb9f1b04 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java @@ -1,8 +1,13 @@ package de.thedevstack.conversationsplus.services.filetransfer.http.delete; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import de.thedevstack.conversationsplus.ui.listeners.SimpleUserDecisionCallback; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; import de.thedevstack.conversationsplus.utils.XmppSendUtil; import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp; import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.FileTransferHttpDeleteSlotRequestPacketGenerator; @@ -10,7 +15,7 @@ import de.thedevstack.conversationsplus.xmpp.jid.Jid; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; /** - * Created by steckbrief on 21.08.2016. + * */ public class DeleteRemoteFileService implements SimpleUserDecisionCallback { private Message message; @@ -23,14 +28,14 @@ public class DeleteRemoteFileService implements SimpleUserDecisionCallback { if (this.message.isHttpUploaded()) { String path = this.message.getBody(); if (this.message.hasFileOnRemoteHost()) { - path = this.message.getFileParams().url.toString(); + path = this.message.getFileParams().getUrl(); } DeleteRemoteFile remoteFile = new DeleteRemoteFile(path, this.message); Account account = this.message.getConversation().getAccount(); Jid host = account.getXmppConnection().findDiscoItemByFeature(FileTransferHttp.NAMESPACE); IqPacket request = FileTransferHttpDeleteSlotRequestPacketGenerator.generate(host, path); - + MessageUtil.setAndSaveFileStatus(this.message, FileStatus.DELETING); XmppSendUtil.sendIqPacket(account, request, new DeleteTokenReceived(remoteFile)); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java index a8bef0ed..186454e4 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java @@ -7,7 +7,7 @@ import java.io.IOException; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.entities.Account; -import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.http.HttpClient; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; @@ -21,7 +21,7 @@ import okhttp3.Request; import okhttp3.Response; /** - * Created by steckbrief on 21.08.2016. + * */ public class DeleteTokenReceived implements OnIqPacketReceived { private static final String HEADER_NAME_DELETE_TOKEN = "X-FILETRANSFER-HTTP-DELETE-TOKEN"; @@ -47,19 +47,19 @@ public class DeleteTokenReceived implements OnIqPacketReceived { @Override public void onFailure(Call call, IOException e) { Logging.e("filetransfer.http.delete", "Error while connecting to '" + call.request().url() + "': " + e.getMessage()); - MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); + MessageUtil.setAndSaveFileStatus(remoteFile.getMessage(), FileStatus.DELETE_FAILED); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { - MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETED); + MessageUtil.setAndSaveFileStatus(remoteFile.getMessage(), FileStatus.DELETED); Logging.i("filetransfer.http.delete", "Remote file successfully deleted '" + remoteFile.getPath() + "'"); } else { String detailedMessage = response.body().string(); + FileStatus fileStatus = FileStatus.DELETE_FAILED; switch (response.code()) { case 403: - case 404: case 500: try { JSONObject jsonObject = new JSONObject(detailedMessage); @@ -68,16 +68,20 @@ public class DeleteTokenReceived implements OnIqPacketReceived { Logging.e("filetransfer.http.delete", "Failed to get error message from expected json response: " + detailedMessage); } break; + case 404: + fileStatus = FileStatus.DELETED; + Logging.i("filetransfer.http.delete", "Failed to delete file - it was already deleted."); + break; } Logging.e("filetransfer.http.delete", "Could not delete remote file '" + remoteFile.getPath() + "'. Response Code: " + response.code() + ", details: " + detailedMessage); - MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); + MessageUtil.setAndSaveFileStatus(remoteFile.getMessage(), fileStatus); } } }); } catch (XmppException e) { Logging.e("filetransfer.http.delete", "Error while trying to get the delete token: " + e.getMessage()); - MessageUtil.markMessage(remoteFile.getMessage(), Message.STATUS_REMOTE_FILE_DELETE_FAILED); + MessageUtil.setAndSaveFileStatus(remoteFile.getMessage(), FileStatus.DELETE_FAILED); } } } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java index fbe7186b..a373d584 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java @@ -6,7 +6,9 @@ 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.FileParams; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.services.filetransfer.FileTransferEntity; import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason; import de.thedevstack.conversationsplus.utils.CryptoHelper; @@ -24,7 +26,13 @@ public class HttpFileTransferEntity extends FileTransferEntity { public HttpFileTransferEntity(Message message, boolean delayed) { super(message); this.getMessage().setHttpUploaded(true); - this.getMessage().setNoDownloadable(); + this.getMessage().setNoDownloadable(); // TODO Set rmeote file status to uploaded + FileParams fileParams = this.getMessage().getFileParams(); + if (null == fileParams) { + fileParams = new FileParams(); + } + fileParams.setFileStatus(FileStatus.NEEDS_UPLOAD); + this.getMessage().setFileParams(fileParams); if (Config.ENCRYPT_ON_HTTP_UPLOADED || message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_OTR) { @@ -77,6 +85,7 @@ public class HttpFileTransferEntity extends FileTransferEntity { getUrl = new URL(getUrl.toString() + "#" + CryptoHelper.bytesToHex(this.getKey())); } + this.getMessage().getFileParams().setFileStatus(FileStatus.UPLOADED); MessageUtil.updateFileParams(this.getMessage(), getUrl); } catch (MalformedURLException e) { Logging.e("httpupload", "Not a valid get url"); diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java index 318a8b8f..8cae599c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java @@ -84,6 +84,7 @@ public class HttpFileUploader implements Runnable { // Send the URL to the counterpart Message message = this.entity.getMessage(); + message.setBody(this.entity.getGetUrl()); message.setCounterpart(message.getConversation().getJid().toBareJid()); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { XmppConnectionServiceAccessor.xmppConnectionService.getPgpEngine().encrypt(message, new HttpUploadedFileEncryptionUiCallback(this.entity)); diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java index f08e27f5..6b19cd5f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java @@ -86,6 +86,7 @@ public class HttpUploadFileTransferService extends AbstractFileTransferService i return null != message && null != message.getConversation() && null != message.getConversation().getAccount() - && message.getConversation().getAccount().httpUploadAvailable(FileBackend.getFile(message, false).getSize()); + && null != message.getFileParams() + && message.getConversation().getAccount().httpUploadAvailable(message.getFileParams().getSize()); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java index 56065ea4..0c2f2960 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java @@ -578,7 +578,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa deleteFile.setVisible(true); deleteFile.setTitle(activity.getString(R.string.delete_x_file,UIHelper.getFileDescriptionString(activity, m))); } - if (m.isHttpUploaded() && (MessageUtil.isMessageSent(m) || m.getStatus() == Message.STATUS_REMOTE_FILE_DELETE_FAILED)) { + if (m.isHttpUploaded() && MessageUtil.isMessageSent(m)) { MenuItem deleteRemoteFile = menu.findItem(R.id.msg_ctx_menu_delete_remote_file); deleteRemoteFile.setVisible(true); } @@ -676,7 +676,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa url = message.getBody(); } else if (message.hasFileOnRemoteHost()) { resId = R.string.file_url; - url = message.getFileParams().url.toString(); + url = message.getFileParams().getUrl(); } else { url = message.getBody(); resId = R.string.file_url; diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java index e28774f2..98a9a96c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java @@ -90,7 +90,7 @@ public class ConversationAdapter extends ArrayAdapter { convName.setTypeface(null, Typeface.NORMAL); } - if (message.getFileParams().width > 0 + if ((null != message.getFileParams() && message.getFileParams().getWidth() > 0) // TODO: Use FileParams.getMimeType() && (message.getTransferable() == null || message.getTransferable().getStatus() != Transferable.STATUS_DELETED)) { mLastMessage.setVisibility(View.GONE); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java index b2dca6b9..e93ddbfa 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java @@ -20,6 +20,7 @@ import android.text.style.StyleSpan; import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Patterns; +import android.view.Gravity; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -46,15 +47,17 @@ import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlSession; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.entities.Message.FileParams; import de.thedevstack.conversationsplus.entities.Transferable; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.providers.ConversationsPlusFileProvider; import de.thedevstack.conversationsplus.services.AvatarService; import de.thedevstack.conversationsplus.ui.ConversationActivity; import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.utils.GeoHelper; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.UIHelper; public class MessageAdapter extends ArrayAdapter { @@ -138,13 +141,17 @@ public class MessageAdapter extends ArrayAdapter { boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() <= Message.STATUS_RECEIVED; - if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getTransferable() != null) { + if (message.hasFileAttached() || message.getTransferable() != null) { FileParams params = message.getFileParams(); - if (params.size > (1.5 * 1024 * 1024)) { - filesize = params.size / (1024 * 1024)+ " MiB"; - } else if (params.size > 0) { - filesize = params.size / 1024 + " KiB"; - } + if (null != params) { + long size = params.getSize(); + if (size > (1.5 * 1024 * 1024)) { + filesize = size / (1024 * 1024)+ " MiB"; + } else if (size > 0) { + filesize = size / 1024 + " KiB"; + } + } + if (message.getTransferable() != null && message.getTransferable().getStatus() == Transferable.STATUS_FAILED) { error = true; } @@ -247,6 +254,25 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.time.setText(formatedTime); } } + + if (message.hasFileAttached() && null != message.getFileParams() && null != viewHolder.remoteFileStatus) { + FileStatus fileStatus = message.getFileParams().getFileStatus(); + if (fileStatus == FileStatus.DELETE_FAILED || fileStatus == FileStatus.DELETED || fileStatus == FileStatus.DELETING) { + viewHolder.remoteFileStatus.setVisibility(View.VISIBLE); + viewHolder.remoteFileStatus.setTypeface(null, Typeface.ITALIC); + switch (fileStatus) { + case DELETE_FAILED: + viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_failed); + break; + case DELETED: + viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_success); + break; + case DELETING: + viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_inprogress); + break; + } + } + } } private void displayInfoMessage(ViewHolder viewHolder, String text, boolean darkBackground) { @@ -434,9 +460,10 @@ public class MessageAdapter extends ArrayAdapter { } viewHolder.messageBody.setVisibility(View.GONE); viewHolder.image.setVisibility(View.VISIBLE); - FileParams params = message.getFileParams(); //TODO: Check what value add the following lines have (compared with setting height/width in XmppActivity.loadBitmap from thumbnail after thumbnail is created) - /*double target = metrics.density * 288; + /* + FileParams params = message.getFileParams(); + double target = metrics.density * 288; int scalledW; int scalledH; if (params.width <= params.height) { @@ -491,6 +518,7 @@ public class MessageAdapter extends ArrayAdapter { .findViewById(R.id.message_time); viewHolder.indicatorReceived = (ImageView) view .findViewById(R.id.indicator_received); + viewHolder.remoteFileStatus = (TextView) view.findViewById(R.id.remote_file_status); break; case RECEIVED: view = activity.getLayoutInflater().inflate( @@ -585,7 +613,7 @@ public class MessageAdapter extends ArrayAdapter { } else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { displayImageMessage(viewHolder, message); } else if (message.getType() == Message.TYPE_FILE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { - if (message.getFileParams().width > 0) { + if (message.getFileParams() != null && message.getFileParams().getWidth() > 0) { // TODO Use FileParams.getMimeType() displayImageMessage(viewHolder,message); } else { displayOpenableMessage(viewHolder, message); @@ -612,12 +640,20 @@ public class MessageAdapter extends ArrayAdapter { } } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { displayDecryptionFailed(viewHolder,darkBackground); - } else { + } else if (message.hasFileAttached()) { // TODO: Move to a single block with Message.TYPE_FILE OR Message.TYPE_IMAGE + FileParams fileParams = message.getFileParams(); + String mimeType = (null != fileParams) ? fileParams.getMimeType() : null; + if (null != mimeType && mimeType.startsWith("image/")) { + displayImageMessage(viewHolder, message); + } else { + displayOpenableMessage(viewHolder, message); + } + } else { if (GeoHelper.isGeoUri(message.getBody())) { displayLocationMessage(viewHolder,message); - } else if (message.treatAsDownloadable() == Message.Decision.MUST) { + } else if (MessageUtil.needsDownload(message)) { try { - URL url = new URL(message.getBody()); + URL url = new URL(message.getFileParams().getUrl()); displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize_on_host, @@ -724,7 +760,8 @@ public class MessageAdapter extends ArrayAdapter { protected ImageView contact_picture; protected TextView status_message; protected TextView encryption; - } + public TextView remoteFileStatus; + } class BitmapWorkerTask extends AsyncTask { private final WeakReference imageViewReference; diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java index e1c30a56..4ec8e3eb 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java @@ -11,6 +11,7 @@ import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusColors; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.utils.UIHelper; import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; @@ -59,10 +60,10 @@ public class MessageDetailsDialog extends AbstractAlertDialog { Logging.d("messagedetailsfile", "File is stored in path: " + message.getRelativeFilePath()); view.findViewById(R.id.dlgMsgDetFileTable).setVisibility(View.VISIBLE); if (null != message.getFileParams()) { - Message.FileParams params = message.getFileParams(); - TextViewUtil.setText(view, R.id.dlgMsgDetFileSize, UIHelper.getHumanReadableFileSize(params.size)); + FileParams params = message.getFileParams(); + TextViewUtil.setText(view, R.id.dlgMsgDetFileSize, UIHelper.getHumanReadableFileSize(params.getSize())); + TextViewUtil.setText(view, R.id.dlgMsgDetFileMimeType, params.getMimeType()); } - TextViewUtil.setText(view, R.id.dlgMsgDetFileMimeType, message.getMimeType()); TextViewUtil.setText(view, R.id.dlgMsgDetFileHttpUploaded, message.isHttpUploaded() ? R.string.cplus_yes : R.string.cplus_no); } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java index b7340ce0..1574bb85 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java @@ -14,6 +14,8 @@ import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.crypto.PgpEngine; +import de.thedevstack.conversationsplus.entities.FileParams; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.enums.UserDecision; import de.thedevstack.conversationsplus.exceptions.UiException; import de.thedevstack.conversationsplus.utils.FileUtils; @@ -27,6 +29,7 @@ import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.ui.UiCallback; import de.thedevstack.conversationsplus.ui.XmppActivity; +import de.thedevstack.conversationsplus.utils.MimeUtils; import de.thedevstack.conversationsplus.utils.StreamUtil; /** @@ -96,20 +99,18 @@ public class ResizePictureUserDecisionListener implements UserDecisionListener { @Override public void onYes() { this.showPrepareFileToast(); - final Message message; - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); - } else { - message = new Message(conversation, "", conversation.getNextEncryption()); - } - message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_IMAGE); + final Message message = createMessage(); ConversationsPlusApplication.executeFileAdding(new OnYesRunnable(message, uri)); } @Override public void onNo() { this.showPrepareFileToast(); + final Message message = createMessage(); + ConversationsPlusApplication.executeFileAdding(new OnNoRunnable(message, uri)); + } + + protected Message createMessage() { final Message message; if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); @@ -117,8 +118,11 @@ public class ResizePictureUserDecisionListener implements UserDecisionListener { message = new Message(conversation, "", conversation.getNextEncryption()); } message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_IMAGE); - ConversationsPlusApplication.executeFileAdding(new OnNoRunnable(message, uri)); + //message.setType(Message.TYPE_IMAGE); + message.setFileParams(new FileParams()); + message.getFileParams().setFileStatus(FileStatus.NEEDS_UPLOAD); + + return message; } @Override diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java index 77c2c728..55fd1b8e 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java @@ -6,6 +6,7 @@ import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.crypto.PgpEngine; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.exceptions.FileCopyException; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.ui.UiCallback; @@ -43,10 +44,12 @@ public class ConversationUtil { message = new Message(conversation, "", conversation.getNextEncryption()); } message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_FILE); + //message.setType(Message.TYPE_FILE); + message.getFileParams().setFileStatus(FileStatus.NEEDS_UPLOAD); String path = FileUtils.getPath(uri); if (path != null) { - message.setRelativeFilePath(path); + message.getFileParams().setPath(path); + message.setRelativeFilePath(path); // TODO: Remove when everything is moved to fileparams MessageUtil.updateFileParams(message); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { PgpEngine.getInstance().encrypt(message, callback); diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/FileUtils.java b/src/main/java/de/thedevstack/conversationsplus/utils/FileUtils.java index 3ee15f70..ce289968 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/FileUtils.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/FileUtils.java @@ -16,6 +16,7 @@ import java.io.File; import java.util.List; import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.entities.Transferable; public final class FileUtils { @@ -160,6 +161,27 @@ public final class FileUtils { return "com.android.providers.media.documents".equals(uri.getAuthority()); } + public static String getRelevantExtension(String path) { + if (path == null || path.isEmpty()) { + return null; + } + + String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase(); + + final String lastPart = FileUtils.getLastExtension(filename); + + if (!lastPart.isEmpty()) { + // we want the real file extension, not the crypto one + final String secondToLastPart = FileUtils.getSecondToLastExtension(filename); + if (!secondToLastPart.isEmpty() && Transferable.VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { + return secondToLastPart; + } else { + return lastPart; + } + } + return null; + } + /** * @param filename The filename to extract extension from * @return last extension or empty string diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java index bf79d7e0..0b8ace95 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java @@ -337,20 +337,28 @@ public final class ImageUtil { public static int calcSampleSize(Uri image, int size) throws FileNotFoundException { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(StreamUtil.openInputStreamFromContentResolver(image), null, options); - return calcSampleSize(options, size); + Bitmap bmp = BitmapFactory.decodeStream(StreamUtil.openInputStreamFromContentResolver(image), null, options); + int height = options.outHeight; + int width = options.outWidth; + if (null != bmp) { + bmp.recycle(); + } + return calcSampleSize(width, height, size); } public static int calcSampleSize(File image, int size) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(image.getAbsolutePath(), options); - return calcSampleSize(options, size); - } - - public static int calcSampleSize(BitmapFactory.Options options, int size) { + Bitmap bmp = BitmapFactory.decodeFile(image.getAbsolutePath(), options); int height = options.outHeight; int width = options.outWidth; + if (null != bmp) { + bmp.recycle(); + } + return calcSampleSize(width, height, size); + } + + private static int calcSampleSize(int width, int height, int size) { int inSampleSize = 1; if (height > size || width > size) { diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java index 626b1bf3..dc9607f0 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java @@ -1,5 +1,6 @@ package de.thedevstack.conversationsplus.utils; +import android.graphics.Bitmap; import android.graphics.BitmapFactory; import java.net.URL; @@ -9,7 +10,9 @@ import java.util.regex.Pattern; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import de.thedevstack.conversationsplus.persistance.FileBackend; @@ -18,6 +21,13 @@ import de.thedevstack.conversationsplus.persistance.FileBackend; */ public final class MessageUtil { + public static boolean needsDownload(Message message) { + FileStatus fileStatus = (null != message.getFileParams()) ? message.getFileParams().getFileStatus() : null; + return (null != fileStatus && (fileStatus == FileStatus.NEEDS_DOWNLOAD + || fileStatus == FileStatus.UNDEFINED)) + && message.treatAsDownloadable() != Message.Decision.NEVER; + } + public static boolean isMessageSent(Message message) { switch (message.getStatus()) { case Message.STATUS_SEND: @@ -29,6 +39,12 @@ public final class MessageUtil { } } + public static void setAndSaveFileStatus(Message message, FileStatus fileStatus) { + message.getFileParams().setFileStatus(fileStatus); + DatabaseBackend.getInstance().updateMessage(message); + UiUpdateHelper.updateConversationUi(); + } + public static boolean markMessage(Conversation conversation, String uuid, int status) { if (uuid == null) { return false; @@ -76,7 +92,7 @@ public final class MessageUtil { public static void updateMessageWithImageDetails(Message message, String filePath, long size, int imageWidth, int imageHeight) { message.setRelativeFilePath(filePath); - MessageUtil.updateMessageBodyWithImageParams(message, size, imageWidth, imageHeight); + MessageUtil.updateMessageWithFileParams(message, null, size, imageWidth, imageHeight); } public static void updateFileParams(Message message) { @@ -90,42 +106,37 @@ public final class MessageUtil { if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); + Bitmap bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options); imageHeight = options.outHeight; imageWidth = options.outWidth; + if (null != bmp) { + bmp.recycle(); + } } - MessageUtil.updateMessageBodyWithFileParams(message, url, file.getSize(), imageWidth, imageHeight); - } - - private static void updateMessageBodyWithFileParams(Message message, URL url, long fileSize, int imageWidth, int imageHeight) { - message.setBody(MessageUtil.getMessageBodyWithImageParams(url, fileSize, imageWidth, imageHeight)); - } - - private static void updateMessageBodyWithImageParams(Message message, long size, int imageWidth, int imageHeight) { - MessageUtil.updateMessageBodyWithImageParams(message, null, size, imageWidth, imageHeight); + MessageUtil.updateMessageWithFileParams(message, url, file.getSize(), imageWidth, imageHeight); } - private static void updateMessageBodyWithImageParams(Message message, URL url, long size, int imageWidth, int imageHeight) { - message.setBody(MessageUtil.getMessageBodyWithImageParams(url, size, imageWidth, imageHeight)); - } - - private static String getMessageBodyWithImageParams(URL url, long size, int imageWidth, int imageHeight) { - StringBuilder sb = new StringBuilder(); + private static void updateMessageWithFileParams(Message message, URL url, long size, int imageWidth, int imageHeight) { + FileParams fileParams = message.getFileParams(); + if (null == fileParams) { + fileParams = new FileParams(); + } + fileParams.setSize(size); if (null != url) { - sb.append(url.toString()); - sb.append('|'); + fileParams.setUrl(url.toString()); } - sb.append(size); if (-1 < imageWidth) { - sb.append('|'); - sb.append(imageWidth); + fileParams.setWidth(imageWidth); } if (-1 < imageHeight) { - sb.append('|'); - sb.append(imageHeight); + fileParams.setHeight(imageHeight); + } + String relativeFilePathFromMessage = message.getRelativeFilePath(); + if (null != relativeFilePathFromMessage && relativeFilePathFromMessage.startsWith("/")) { + fileParams.setPath(relativeFilePathFromMessage); } - return sb.toString(); + message.setFileParams(fileParams); } private MessageUtil() { diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml index 0edbab51..30003309 100644 --- a/src/main/res/layout/message_sent.xml +++ b/src/main/res/layout/message_sent.xml @@ -103,6 +103,18 @@ android:gravity="center_vertical" android:src="@drawable/ic_received_indicator" /> + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 8136a84f..e356a947 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -676,4 +676,7 @@ Share URI with… Delete remote file Are you sure? + Failed to delete remote file + Remote file deleted + Deleting remote file... -- cgit v1.2.3 From 9ec29bb1dcf664fea606105b7e700641a4b44ae4 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Thu, 29 Sep 2016 11:58:17 +0200 Subject: Fixes FS#29: Own Avatar not loaded --- .../thedevstack/conversationsplus/parser/PresenceParser.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java index f575aaac..e86e657e 100644 --- a/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import de.thedevstack.conversationsplus.utils.AvatarUtil; import de.thedevstack.conversationsplus.utils.UiUpdateHelper; @@ -182,14 +183,19 @@ public class PresenceParser extends AbstractParser implements final String resource = from.isBareJid() ? "" : from.getResourcepart(); contact.setPresenceName(packet.findChildContent("nick", "http://jabber.org/protocol/nick")); Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); - if (avatar != null && !contact.isSelf()) { + if (avatar != null && (!contact.isSelf() || null == account.getAvatar())) { avatar.owner = from.toBareJid(); if (AvatarUtil.isAvatarCached(avatar)) { - if (contact.setAvatar(avatar)) { + if (avatar.owner.equals(account.getJid().toBareJid())) { + account.setAvatar(avatar.getFilename()); + DatabaseBackend.getInstance().updateAccount(account); + AvatarService.getInstance().clear(account); + UiUpdateHelper.updateAccountUi(); + } else if (contact.setAvatar(avatar)) { AvatarService.getInstance().clear(contact); - UiUpdateHelper.updateConversationUi(); UiUpdateHelper.updateRosterUi(); } + UiUpdateHelper.updateConversationUi(); } else { AvatarService.getInstance().fetchAvatar(account, avatar); } -- cgit v1.2.3