Add compression support

This commit is contained in:
Rene Treffer 2014-04-03 18:16:14 +02:00
parent 2506ef82df
commit 9502ff25dd
8 changed files with 227 additions and 10 deletions

View File

@ -50,7 +50,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Use Transport Layer Security (TLS)" android:text="Use Transport Layer Security (TLS)"
android:checked="true"/> 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 <CheckBox
android:id="@+id/edit_account_register_new" android:id="@+id/edit_account_register_new"

View File

@ -30,6 +30,7 @@ public class Account extends AbstractEntity{
public static final int OPTION_USETLS = 0; public static final int OPTION_USETLS = 0;
public static final int OPTION_DISABLED = 1; public static final int OPTION_DISABLED = 1;
public static final int OPTION_REGISTER = 2; 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_CONNECTING = 0;
public static final int STATUS_DISABLED = -2; public static final int STATUS_DISABLED = -2;

View File

@ -43,7 +43,9 @@ public class EditAccount extends DialogFragment {
final EditText jidText = (EditText) view.findViewById(R.id.account_jid); final EditText jidText = (EditText) view.findViewById(R.id.account_jid);
final TextView confirmPwDesc = (TextView) view final TextView confirmPwDesc = (TextView) view
.findViewById(R.id.account_confirm_password_desc); .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 final EditText password = (EditText) view
.findViewById(R.id.account_password); .findViewById(R.id.account_password);
@ -57,11 +59,8 @@ public class EditAccount extends DialogFragment {
if (account != null) { if (account != null) {
jidText.setText(account.getJid()); jidText.setText(account.getJid());
password.setText(account.getPassword()); password.setText(account.getPassword());
if (account.isOptionSet(Account.OPTION_USETLS)) { useTLS.setChecked(account.isOptionSet(Account.OPTION_USETLS));
useTLS.setChecked(true); useCompression.setChecked(account.isOptionSet(Account.OPTION_USETLS));
} else {
useTLS.setChecked(false);
}
Log.d("xmppService","mein debugger. account != null"); Log.d("xmppService","mein debugger. account != null");
if (account.isOptionSet(Account.OPTION_REGISTER)) { if (account.isOptionSet(Account.OPTION_REGISTER)) {
registerAccount.setChecked(true); registerAccount.setChecked(true);
@ -122,6 +121,7 @@ public class EditAccount extends DialogFragment {
.findViewById(R.id.account_password); .findViewById(R.id.account_password);
String password = passwordEdit.getText().toString(); String password = passwordEdit.getText().toString();
CheckBox useTLS = (CheckBox) d.findViewById(R.id.account_usetls); 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); CheckBox register = (CheckBox) d.findViewById(R.id.edit_account_register_new);
String username; String username;
String server; String server;
@ -141,6 +141,7 @@ public class EditAccount extends DialogFragment {
account = new Account(username, server, password); account = new Account(username, server, password);
} }
account.setOption(Account.OPTION_USETLS, useTLS.isChecked()); account.setOption(Account.OPTION_USETLS, useTLS.isChecked());
account.setOption(Account.OPTION_USECOMPRESSION, useCompression.isChecked());
account.setOption(Account.OPTION_REGISTER, register.isChecked()); account.setOption(Account.OPTION_REGISTER, register.isChecked());
if (listener != null) { if (listener != null) {
listener.onAccountEdited(account); listener.onAccountEdited(account);

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
public class TagWriter { public class TagWriter {
private OutputStream plainOutputStream;
private OutputStreamWriter outputStream; private OutputStreamWriter outputStream;
private boolean finshed = false; private boolean finshed = false;
private LinkedBlockingQueue<AbstractStanza> writeQueue = new LinkedBlockingQueue<AbstractStanza>(); private LinkedBlockingQueue<AbstractStanza> writeQueue = new LinkedBlockingQueue<AbstractStanza>();
@ -37,9 +38,14 @@ public class TagWriter {
} }
public void setOutputStream(OutputStream out) { public void setOutputStream(OutputStream out) {
this.plainOutputStream = out;
this.outputStream = new OutputStreamWriter(out); this.outputStream = new OutputStreamWriter(out);
} }
public OutputStream getOutputStream() {
return this.plainOutputStream;
}
public TagWriter beginDocument() throws IOException { public TagWriter beginDocument() throws IOException {
outputStream.write("<?xml version='1.0'?>"); outputStream.write("<?xml version='1.0'?>");
outputStream.flush(); outputStream.flush();

View File

@ -36,7 +36,11 @@ public class XmlReader {
Log.d(LOGTAG,"error setting input stream"); Log.d(LOGTAG,"error setting input stream");
} }
} }
public InputStream getInputStream() {
return is;
}
public void reset() { public void reset() {
try { try {
parser.setInput(new InputStreamReader(this.is)); parser.setInput(new InputStreamReader(this.is));

View File

@ -38,6 +38,8 @@ import android.util.Log;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.DNSHelper; 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.Element;
import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
@ -177,6 +179,13 @@ public class XmppConnection implements Runnable {
wakeLock.release(); wakeLock.release();
} }
return; 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) { } catch (XmlPullParserException e) {
this.changeStatus(Account.STATUS_OFFLINE); this.changeStatus(Account.STATUS_OFFLINE);
Log.d(LOGTAG, "xml exception " + e.getMessage()); Log.d(LOGTAG, "xml exception " + e.getMessage());
@ -194,7 +203,7 @@ public class XmppConnection implements Runnable {
} }
private void processStream(Tag currentTag) throws XmlPullParserException, private void processStream(Tag currentTag) throws XmlPullParserException,
IOException { IOException, NoSuchAlgorithmException {
Tag nextTag = tagReader.readTag(); Tag nextTag = tagReader.readTag();
while ((nextTag != null) && (!nextTag.isEnd("stream"))) { while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
if (nextTag.isStart("error")) { if (nextTag.isStart("error")) {
@ -208,6 +217,8 @@ public class XmppConnection implements Runnable {
} }
} else if (nextTag.isStart("proceed")) { } else if (nextTag.isStart("proceed")) {
switchOverToTls(nextTag); switchOverToTls(nextTag);
} else if (nextTag.isStart("compressed")) {
switchOverToZLib(nextTag);
} else if (nextTag.isStart("success")) { } else if (nextTag.isStart("success")) {
Log.d(LOGTAG, account.getJid() Log.d(LOGTAG, account.getJid()
+ ": logged in"); + ": 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 { private void sendStartTLS() throws IOException {
Tag startTLS = Tag.empty("starttls"); Tag startTLS = Tag.empty("starttls");
startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
@ -486,6 +524,8 @@ public class XmppConnection implements Runnable {
if (this.streamFeatures.hasChild("starttls") if (this.streamFeatures.hasChild("starttls")
&& account.isOptionSet(Account.OPTION_USETLS)) { && account.isOptionSet(Account.OPTION_USETLS)) {
sendStartTLS(); sendStartTLS();
} else if (compressionAvailable()) {
sendCompressionZlib();
} else if (this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) { } else if (this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
sendRegistryRequest(); sendRegistryRequest();
} else if (!this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) { } 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) { private List<String> extractMechanisms(Element stream) {
ArrayList<String> mechanisms = new ArrayList<String>(stream.getChildren().size()); ArrayList<String> mechanisms = new ArrayList<String>(stream.getChildren().size());
for(Element child : stream.getChildren()) { for(Element child : stream.getChildren()) {