aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRene Treffer <treffer@measite.de>2014-04-03 18:16:14 +0200
committerRene Treffer <treffer@measite.de>2014-04-03 18:37:25 +0200
commit9502ff25dd6c32c92d356baaaa1640fe26314595 (patch)
tree7767f2193bd2233b3275db2342c36d6e08eeac4f
parent2506ef82df6369fef71e00e891b2a2c69330ff94 (diff)
Add compression support
-rw-r--r--res/layout/edit_account_dialog.xml8
-rw-r--r--src/eu/siacs/conversations/entities/Account.java1
-rw-r--r--src/eu/siacs/conversations/ui/EditAccount.java13
-rw-r--r--src/eu/siacs/conversations/utils/zlib/ZLibInputStream.java52
-rw-r--r--src/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java89
-rw-r--r--src/eu/siacs/conversations/xml/TagWriter.java8
-rw-r--r--src/eu/siacs/conversations/xml/XmlReader.java6
-rw-r--r--src/eu/siacs/conversations/xmpp/XmppConnection.java60
8 files changed, 227 insertions, 10 deletions
diff --git a/res/layout/edit_account_dialog.xml b/res/layout/edit_account_dialog.xml
index 2b1f95ab..cc29b6f9 100644
--- a/res/layout/edit_account_dialog.xml
+++ b/res/layout/edit_account_dialog.xml
@@ -50,7 +50,13 @@
android:layout_height="wrap_content"
android:text="Use Transport Layer Security (TLS)"
android:checked="true"/>
-
+
+ <CheckBox
+ android:id="@+id/account_usecompression"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Use Compression (zlib)"
+ android:checked="true"/>
<CheckBox
android:id="@+id/edit_account_register_new"
diff --git a/src/eu/siacs/conversations/entities/Account.java b/src/eu/siacs/conversations/entities/Account.java
index 7c12c69b..41132b1f 100644
--- a/src/eu/siacs/conversations/entities/Account.java
+++ b/src/eu/siacs/conversations/entities/Account.java
@@ -30,6 +30,7 @@ public class Account extends AbstractEntity{
public static final int OPTION_USETLS = 0;
public static final int OPTION_DISABLED = 1;
public static final int OPTION_REGISTER = 2;
+ public static final int OPTION_USECOMPRESSION = 3;
public static final int STATUS_CONNECTING = 0;
public static final int STATUS_DISABLED = -2;
diff --git a/src/eu/siacs/conversations/ui/EditAccount.java b/src/eu/siacs/conversations/ui/EditAccount.java
index 080b09ea..cdebec54 100644
--- a/src/eu/siacs/conversations/ui/EditAccount.java
+++ b/src/eu/siacs/conversations/ui/EditAccount.java
@@ -43,7 +43,9 @@ public class EditAccount extends DialogFragment {
final EditText jidText = (EditText) view.findViewById(R.id.account_jid);
final TextView confirmPwDesc = (TextView) view
.findViewById(R.id.account_confirm_password_desc);
- CheckBox useTLS = (CheckBox) view.findViewById(R.id.account_usetls);
+ final CheckBox useTLS = (CheckBox) view.findViewById(R.id.account_usetls);
+
+ final CheckBox useCompression = (CheckBox) view.findViewById(R.id.account_usecompression);
final EditText password = (EditText) view
.findViewById(R.id.account_password);
@@ -57,11 +59,8 @@ public class EditAccount extends DialogFragment {
if (account != null) {
jidText.setText(account.getJid());
password.setText(account.getPassword());
- if (account.isOptionSet(Account.OPTION_USETLS)) {
- useTLS.setChecked(true);
- } else {
- useTLS.setChecked(false);
- }
+ useTLS.setChecked(account.isOptionSet(Account.OPTION_USETLS));
+ useCompression.setChecked(account.isOptionSet(Account.OPTION_USETLS));
Log.d("xmppService","mein debugger. account != null");
if (account.isOptionSet(Account.OPTION_REGISTER)) {
registerAccount.setChecked(true);
@@ -122,6 +121,7 @@ public class EditAccount extends DialogFragment {
.findViewById(R.id.account_password);
String password = passwordEdit.getText().toString();
CheckBox useTLS = (CheckBox) d.findViewById(R.id.account_usetls);
+ CheckBox useCompression = (CheckBox) d.findViewById(R.id.account_usecompression);
CheckBox register = (CheckBox) d.findViewById(R.id.edit_account_register_new);
String username;
String server;
@@ -141,6 +141,7 @@ public class EditAccount extends DialogFragment {
account = new Account(username, server, password);
}
account.setOption(Account.OPTION_USETLS, useTLS.isChecked());
+ account.setOption(Account.OPTION_USECOMPRESSION, useCompression.isChecked());
account.setOption(Account.OPTION_REGISTER, register.isChecked());
if (listener != null) {
listener.onAccountEdited(account);
diff --git a/src/eu/siacs/conversations/utils/zlib/ZLibInputStream.java b/src/eu/siacs/conversations/utils/zlib/ZLibInputStream.java
new file mode 100644
index 00000000..2eebf6f4
--- /dev/null
+++ b/src/eu/siacs/conversations/utils/zlib/ZLibInputStream.java
@@ -0,0 +1,52 @@
+package eu.siacs.conversations.utils.zlib;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * ZLibInputStream is a zlib and input stream compatible version of an
+ * InflaterInputStream. This class solves the incompatibility between
+ * {@link InputStream#available()} and {@link InflaterInputStream#available()}.
+ */
+public class ZLibInputStream extends InflaterInputStream {
+
+ /**
+ * Construct a ZLibInputStream, reading data from the underlying stream.
+ *
+ * @param is The {@code InputStream} to read data from.
+ * @throws IOException If an {@code IOException} occurs.
+ */
+ public ZLibInputStream(InputStream is) throws IOException {
+ super(is, new Inflater(), 512);
+ }
+
+ /**
+ * Provide a more InputStream compatible version of available.
+ * A return value of 1 means that it is likly to read one byte without
+ * blocking, 0 means that the system is known to block for more input.
+ *
+ * @return 0 if no data is available, 1 otherwise
+ * @throws IOException
+ */
+ @Override
+ public int available() throws IOException {
+ /* This is one of the funny code blocks.
+ * InflaterInputStream.available violates the contract of
+ * InputStream.available, which breaks kXML2.
+ *
+ * I'm not sure who's to blame, oracle/sun for a broken api or the
+ * google guys for mixing a sun bug with a xml reader that can't handle
+ * it....
+ *
+ * Anyway, this simple if breaks suns distorted reality, but helps
+ * to use the api as intended.
+ */
+ if (inf.needsInput()) {
+ return 0;
+ }
+ return super.available();
+ }
+
+}
diff --git a/src/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java b/src/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java
new file mode 100644
index 00000000..f2bff9fd
--- /dev/null
+++ b/src/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java
@@ -0,0 +1,89 @@
+package eu.siacs.conversations.utils.zlib;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.NoSuchAlgorithmException;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+
+/**
+ * <p>Android 2.2 includes Java7 FLUSH_SYNC option, which will be used by this
+ * Implementation, preferable via reflection. The @hide was remove in API level
+ * 19. This class might thus go away in the future.</p>
+ * <p>Please use {@link ZLibOutputStream#SUPPORTED} to check for flush
+ * compatibility.</p>
+ */
+public class ZLibOutputStream extends DeflaterOutputStream {
+
+ /**
+ * The reflection based flush method.
+ */
+
+ private final static Method method;
+ /**
+ * SUPPORTED is true if a flush compatible method exists.
+ */
+ public final static boolean SUPPORTED;
+
+ /**
+ * Static block to initialize {@link #SUPPORTED} and {@link #method}.
+ */
+ static {
+ Method m = null;
+ try {
+ m = Deflater.class.getMethod("deflate", byte[].class, int.class, int.class, int.class);
+ } catch (SecurityException e) {
+ } catch (NoSuchMethodException e) {
+ }
+ method = m;
+ SUPPORTED = (method != null);
+ }
+
+ /**
+ * Create a new ZLib compatible output stream wrapping the given low level
+ * stream. ZLib compatiblity means we will send a zlib header.
+ * @param os OutputStream The underlying stream.
+ * @throws IOException In case of a lowlevel transfer problem.
+ * @throws NoSuchAlgorithmException In case of a {@link Deflater} error.
+ */
+ public ZLibOutputStream(OutputStream os) throws IOException,
+ NoSuchAlgorithmException {
+ super(os, new Deflater(Deflater.BEST_COMPRESSION));
+ }
+
+ /**
+ * Flush the given stream, preferring Java7 FLUSH_SYNC if available.
+ * @throws IOException In case of a lowlevel exception.
+ */
+ @Override
+ public void flush() throws IOException {
+ if (!SUPPORTED) {
+ super.flush();
+ return;
+ }
+ int count = 0;
+ if (!def.needsInput()) {
+ do {
+ count = def.deflate(buf, 0, buf.length);
+ out.write(buf, 0, count);
+ } while (count > 0);
+ out.flush();
+ }
+ try {
+ do {
+ count = (Integer) method.invoke(def, buf, 0, buf.length, 2);
+ out.write(buf, 0, count);
+ } while (count > 0);
+ } catch (IllegalArgumentException e) {
+ throw new IOException("Can't flush");
+ } catch (IllegalAccessException e) {
+ throw new IOException("Can't flush");
+ } catch (InvocationTargetException e) {
+ throw new IOException("Can't flush");
+ }
+ super.flush();
+ }
+
+}
diff --git a/src/eu/siacs/conversations/xml/TagWriter.java b/src/eu/siacs/conversations/xml/TagWriter.java
index f06664fd..d945b47b 100644
--- a/src/eu/siacs/conversations/xml/TagWriter.java
+++ b/src/eu/siacs/conversations/xml/TagWriter.java
@@ -9,6 +9,7 @@ import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
public class TagWriter {
+ private OutputStream plainOutputStream;
private OutputStreamWriter outputStream;
private boolean finshed = false;
private LinkedBlockingQueue<AbstractStanza> writeQueue = new LinkedBlockingQueue<AbstractStanza>();
@@ -37,9 +38,14 @@ public class TagWriter {
}
public void setOutputStream(OutputStream out) {
+ this.plainOutputStream = out;
this.outputStream = new OutputStreamWriter(out);
}
-
+
+ public OutputStream getOutputStream() {
+ return this.plainOutputStream;
+ }
+
public TagWriter beginDocument() throws IOException {
outputStream.write("<?xml version='1.0'?>");
outputStream.flush();
diff --git a/src/eu/siacs/conversations/xml/XmlReader.java b/src/eu/siacs/conversations/xml/XmlReader.java
index 7ae9a7b3..71e86cf9 100644
--- a/src/eu/siacs/conversations/xml/XmlReader.java
+++ b/src/eu/siacs/conversations/xml/XmlReader.java
@@ -36,7 +36,11 @@ public class XmlReader {
Log.d(LOGTAG,"error setting input stream");
}
}
-
+
+ public InputStream getInputStream() {
+ return is;
+ }
+
public void reset() {
try {
parser.setInput(new InputStreamReader(this.is));
diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java
index 1fd42bca..3702e66b 100644
--- a/src/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -38,6 +38,8 @@ import android.util.Log;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.DNSHelper;
+import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
+import eu.siacs.conversations.utils.zlib.ZLibInputStream;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.TagWriter;
@@ -177,6 +179,13 @@ public class XmppConnection implements Runnable {
wakeLock.release();
}
return;
+ } catch (NoSuchAlgorithmException e) {
+ this.changeStatus(Account.STATUS_OFFLINE);
+ Log.d(LOGTAG, "compression exception " + e.getMessage());
+ if (wakeLock.isHeld()) {
+ wakeLock.release();
+ }
+ return;
} catch (XmlPullParserException e) {
this.changeStatus(Account.STATUS_OFFLINE);
Log.d(LOGTAG, "xml exception " + e.getMessage());
@@ -194,7 +203,7 @@ public class XmppConnection implements Runnable {
}
private void processStream(Tag currentTag) throws XmlPullParserException,
- IOException {
+ IOException, NoSuchAlgorithmException {
Tag nextTag = tagReader.readTag();
while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
if (nextTag.isStart("error")) {
@@ -208,6 +217,8 @@ public class XmppConnection implements Runnable {
}
} else if (nextTag.isStart("proceed")) {
switchOverToTls(nextTag);
+ } else if (nextTag.isStart("compressed")) {
+ switchOverToZLib(nextTag);
} else if (nextTag.isStart("success")) {
Log.d(LOGTAG, account.getJid()
+ ": logged in");
@@ -375,6 +386,33 @@ public class XmppConnection implements Runnable {
}
}
+ private void sendCompressionZlib() throws IOException {
+ tagWriter.writeElement(new Element("compress") {
+ public String toString() {
+ return
+ "<compress xmlns='http://jabber.org/protocol/compress'>"
+ + "<method>zlib</method>"
+ + "</compress>";
+ }
+ });
+ }
+
+ private void switchOverToZLib(Tag currentTag) throws XmlPullParserException,
+ IOException, NoSuchAlgorithmException {
+
+ Log.d(LOGTAG,account.getJid()+": Starting zlib compressed stream");
+
+ tagReader.readTag(); // read tag close
+
+ tagWriter.setOutputStream(new ZLibOutputStream(tagWriter.getOutputStream()));
+ tagReader.setInputStream(new ZLibInputStream(tagReader.getInputStream()));
+
+ sendStartStream();
+ processStream(tagReader.readTag());
+
+ Log.d(LOGTAG,account.getJid()+": zlib compressed stream established");
+ }
+
private void sendStartTLS() throws IOException {
Tag startTLS = Tag.empty("starttls");
startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
@@ -486,6 +524,8 @@ public class XmppConnection implements Runnable {
if (this.streamFeatures.hasChild("starttls")
&& account.isOptionSet(Account.OPTION_USETLS)) {
sendStartTLS();
+ } else if (compressionAvailable()) {
+ sendCompressionZlib();
} else if (this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
sendRegistryRequest();
} else if (!this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
@@ -514,6 +554,24 @@ public class XmppConnection implements Runnable {
}
}
+ private boolean compressionAvailable() {
+ if (!this.streamFeatures.hasChild("compression", "http://jabber.org/features/compress")) return false;
+ if (!ZLibOutputStream.SUPPORTED) return false;
+ if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) return false;
+
+ Element compression = this.streamFeatures.findChild("compression", "http://jabber.org/features/compress");
+ for (Element child : compression.getChildren()) {
+ if (!"method".equals(child.getName())) continue;
+
+ if ("zlib".equalsIgnoreCase(child.getContent())) {
+ Log.d(LOGTAG, account.getJid() + ": compression available");
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private List<String> extractMechanisms(Element stream) {
ArrayList<String> mechanisms = new ArrayList<String>(stream.getChildren().size());
for(Element child : stream.getChildren()) {