Merge branch 'compression' of https://github.com/rtreffer/Conversations into rtreffer-compression

Daniel Gultsch created

Change summary

res/layout/edit_account_dialog.xml                          | 10 
src/eu/siacs/conversations/entities/Account.java            |  1 
src/eu/siacs/conversations/persistance/DatabaseBackend.java | 11 
src/eu/siacs/conversations/ui/EditAccount.java              | 10 
src/eu/siacs/conversations/utils/UIHelper.java              |  2 
src/eu/siacs/conversations/utils/zlib/ZLibInputStream.java  | 52 ++++
src/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java | 89 +++++++
src/eu/siacs/conversations/xml/TagWriter.java               |  8 
src/eu/siacs/conversations/xml/XmlReader.java               |  6 
src/eu/siacs/conversations/xmpp/XmppConnection.java         | 60 ++++
10 files changed, 223 insertions(+), 26 deletions(-)

Detailed changes

res/layout/edit_account_dialog.xml 🔗

@@ -42,16 +42,6 @@
         android:hint="Password" 
         android:fontFamily="sans-serif" />
 
-
-
-    <CheckBox
-        android:id="@+id/account_usetls"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="Use Transport Layer Security (TLS)"
-        android:checked="true"/>
-    
-      
     <CheckBox
         android:id="@+id/edit_account_register_new"
         android:layout_width="wrap_content"

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;

src/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -23,7 +23,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 	private static DatabaseBackend instance = null;
 
 	private static final String DATABASE_NAME = "history";
-	private static final int DATABASE_VERSION = 1;
+	private static final int DATABASE_VERSION = 2;
 
 	public DatabaseBackend(Context context) {
 		super(context, DATABASE_NAME, null, DATABASE_VERSION);
@@ -66,9 +66,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 	}
 
 	@Override
-	public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) {
-		// TODO Auto-generated method stub
-
+	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+		if (oldVersion < 2 && newVersion >= 2) {
+			// enable compression by default.
+			db.execSQL("update " + Account.TABLENAME
+				+ " set " + Account.OPTIONS + " = " + Account.OPTIONS + " | 8");
+		}
 	}
 
 	public static synchronized DatabaseBackend getInstance(Context context) {

src/eu/siacs/conversations/ui/EditAccount.java 🔗

@@ -43,7 +43,6 @@ 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 EditText password = (EditText) view
 				.findViewById(R.id.account_password);
@@ -57,11 +56,6 @@ 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);
-			}
 			Log.d("xmppService","mein debugger. account != null");
 			if (account.isOptionSet(Account.OPTION_REGISTER)) {
 				registerAccount.setChecked(true);
@@ -121,7 +115,6 @@ public class EditAccount extends DialogFragment {
 				EditText passwordEdit = (EditText) d
 						.findViewById(R.id.account_password);
 				String password = passwordEdit.getText().toString();
-				CheckBox useTLS = (CheckBox) d.findViewById(R.id.account_usetls);
 				CheckBox register = (CheckBox) d.findViewById(R.id.edit_account_register_new);
 				String username;
 				String server;
@@ -139,8 +132,9 @@ public class EditAccount extends DialogFragment {
 					account.setServer(server);
 				} else {
 					account = new Account(username, server, password);
+					account.setOption(Account.OPTION_USETLS, true);
+					account.setOption(Account.OPTION_USECOMPRESSION, true);
 				}
-				account.setOption(Account.OPTION_USETLS, useTLS.isChecked());
 				account.setOption(Account.OPTION_REGISTER, register.isChecked());
 				if (listener != null) {
 					listener.onAccountEdited(account);

src/eu/siacs/conversations/utils/UIHelper.java 🔗

@@ -66,7 +66,7 @@ public class UIHelper {
 	}
 
 	private static Bitmap getUnknownContactPicture(String name, int size) {
-		String firstLetter = name.substring(0, 1).toUpperCase(Locale.US);
+		String firstLetter = (name.length() > 0) ? name.substring(0, 1).toUpperCase(Locale.US) : " ";
 
 		int holoColors[] = { 0xFF1da9da, 0xFFb368d9, 0xFF83b600, 0xFFffa713,
 				0xFFe92727 };

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

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

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

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));

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()) {