first draft on xml parser and communication. a long way to go. code definitly not perfect. will refactor asap

Daniel Gultsch created

Change summary

AndroidManifest.xml                                     |   2 
src/de/gultsch/chat/services/XmppConnectionService.java |  41 +
src/de/gultsch/chat/ui/XmppActivity.java                |   1 
src/de/gultsch/chat/utils/SASL.java                     |  24 +
src/de/gultsch/chat/xml/Element.java                    |  65 +++
src/de/gultsch/chat/xml/Tag.java                        |  99 ++++
src/de/gultsch/chat/xml/TagWriter.java                  |  46 ++
src/de/gultsch/chat/xml/XmlReader.java                  |  91 ++++
src/de/gultsch/chat/xmpp/IqPacket.java                  |  26 +
src/de/gultsch/chat/xmpp/XmppConnection.java            | 222 +++++++++++
10 files changed, 616 insertions(+), 1 deletion(-)

Detailed changes

AndroidManifest.xml 🔗

@@ -10,6 +10,8 @@
 
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.READ_PROFILE"/>
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
     
     <application
         android:allowBackup="true"

src/de/gultsch/chat/services/XmppConnectionService.java 🔗

@@ -1,22 +1,43 @@
 package de.gultsch.chat.services;
 
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.UnknownHostException;
 import java.util.List;
 
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
 import de.gultsch.chat.entities.Account;
 import de.gultsch.chat.entities.Contact;
 import de.gultsch.chat.entities.Conversation;
 import de.gultsch.chat.entities.Message;
 import de.gultsch.chat.persistance.DatabaseBackend;
+import de.gultsch.chat.xml.Tag;
+import de.gultsch.chat.xml.XmlReader;
+import de.gultsch.chat.xmpp.XmppConnection;
 import android.app.Service;
+import android.content.Context;
 import android.content.Intent;
 import android.os.Binder;
 import android.os.IBinder;
+import android.os.PowerManager;
 import android.util.Log;
 
 public class XmppConnectionService extends Service {
 	
-	protected static final String LOGTAG = "xmppConnection";
+	protected static final String LOGTAG = "xmppService";
 	protected DatabaseBackend databaseBackend;
+	
+	public long startDate;
+	
+	private List<Account> accounts;
+	
+	public boolean connectionRunnig = false;
 
     private final IBinder mBinder = new XmppConnectionBinder();
 
@@ -26,9 +47,27 @@ public class XmppConnectionService extends Service {
         }
     }
     
+    @Override 
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Log.d(LOGTAG,"recieved start command. been running for "+((System.currentTimeMillis() - startDate) / 1000)+"s");
+        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+        if (!connectionRunnig) {
+	        for(Account account : accounts) {
+	        	Log.d(LOGTAG,"connection wasnt running");
+	        	XmppConnection connection = new XmppConnection(account, pm);
+	        	Thread thread = new Thread(connection);
+	        	thread.start();
+	        }
+	        connectionRunnig = true;
+        }
+        return START_STICKY;
+    }
+    
     @Override
     public void onCreate() {
     	databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
+    	this.accounts = databaseBackend.getAccounts();
+    	startDate = System.currentTimeMillis();
     }
     
     @Override

src/de/gultsch/chat/ui/XmppActivity.java 🔗

@@ -31,6 +31,7 @@ public abstract class XmppActivity extends Activity {
 	
 	@Override
 	protected void onStart() {
+		startService(new Intent(this, XmppConnectionService.class));
 		super.onStart();
 		if (!xmppConnectionServiceBound) {
 			Intent intent = new Intent(this, XmppConnectionService.class);

src/de/gultsch/chat/utils/SASL.java 🔗

@@ -0,0 +1,24 @@
+package de.gultsch.chat.utils;
+
+import android.util.Base64;
+
+public class SASL {
+	public static String plain(String username, String password) {
+		byte[] userBytes = username.getBytes();
+		int userLenght = userBytes.length;
+		byte[] passwordBytes = password.getBytes();
+		byte[] saslBytes = new byte[userBytes.length+passwordBytes.length+2];
+		saslBytes[0] = 0x0;
+		for(int i = 1; i < saslBytes.length; ++i) {
+			if (i<=userLenght) {
+				saslBytes[i] = userBytes[i-1];
+			} else if (i==userLenght+1) {
+				saslBytes[i] = 0x0;
+			} else {
+				saslBytes[i] = passwordBytes[i-(userLenght+2)];
+			}
+		}
+		
+		return Base64.encodeToString(saslBytes, Base64.DEFAULT);
+	}
+}

src/de/gultsch/chat/xml/Element.java 🔗

@@ -0,0 +1,65 @@
+package de.gultsch.chat.xml;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+
+public class Element {
+	protected String name;
+	protected Hashtable<String, String> attributes = new Hashtable<String, String>();
+	protected String content;
+	protected List<Element> children = new ArrayList<Element>();
+	
+	public Element(String name) {
+		this.name = name;
+	}
+	
+	public Element addChild(Element child) {
+		this.content = null;
+		children.add(child);
+		return this;
+	}
+	
+	public Element setContent(String content) {
+		this.content = content;
+		this.children.clear();
+		return this;
+	}
+	
+	public Element setAttribute(String name, String value) {
+		this.attributes.put(name, value);
+		return this;
+	}
+	
+	public Element setAttributes(Hashtable<String, String> attributes) {
+		this.attributes = attributes;
+		return this;
+	}
+	
+	public String toString() {
+		StringBuilder elementOutput = new StringBuilder();
+		if ((content==null)&&(children.size() == 0)) {
+			Tag emptyTag = Tag.empty(name);
+			emptyTag.setAtttributes(this.attributes);
+			elementOutput.append(emptyTag.toString());
+		} else {
+			Tag startTag = Tag.start(name);
+			startTag.setAtttributes(this.attributes);
+			elementOutput.append(startTag);
+			if (content!=null) {
+				elementOutput.append(content);
+			} else {
+				for(Element child : children) {
+					elementOutput.append(child.toString());
+				}
+			}
+			Tag endTag = Tag.end(name);
+			elementOutput.append(endTag);
+		}
+		return elementOutput.toString();
+	}
+
+	public String getName() {
+		return name;
+	}
+}

src/de/gultsch/chat/xml/Tag.java 🔗

@@ -0,0 +1,99 @@
+package de.gultsch.chat.xml;
+
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class Tag {
+	public static final int NO = -1;
+	public static final int START = 0;
+	public static final int END = 1;
+	public static final int EMPTY = 2;
+	
+	protected int type;
+	protected String name;
+	protected Hashtable<String, String> attributes = new Hashtable<String, String>();
+	
+	protected Tag(int type, String name) {
+		this.type = type;
+		this.name = name;
+	}
+
+
+	public static Tag no(String text) {
+		return new Tag(NO,text);
+	}
+	
+	public static Tag start(String name) {
+		return new Tag(START,name);
+	}
+	
+	public static Tag end(String name) {
+		return new Tag(END,name);
+	}
+	
+	public static Tag empty(String name) {
+		return new Tag(EMPTY,name);
+	}
+	
+	public String getName() {
+		return name;
+	}
+	
+	public String getAttribute(String attrName) {
+		return this.attributes.get(attrName);
+	}
+	
+	public Tag setAttribute(String attrName, String attrValue) {
+		this.attributes.put(attrName, attrValue);
+		return this;
+	}
+	
+	public Tag setAtttributes(Hashtable<String, String> attributes) {
+		this.attributes = attributes;
+		return this;
+	}
+	
+	public boolean isStart(String needle) {
+		return (this.type == START) && (this.name.equals(needle));
+	}
+	
+	public boolean isEnd(String needle) {
+		return (this.type == END) && (this.name.equals(needle));
+	}
+	
+	public boolean isNo() {
+		return (this.type == NO);
+	}
+	
+	public String toString() {
+		StringBuilder tagOutput = new StringBuilder();
+		tagOutput.append('<');
+		if (type==END) {
+			tagOutput.append('/');
+		}
+		tagOutput.append(name);
+		if(type!=END) {
+			Set<Entry<String, String>> attributeSet = attributes.entrySet();
+			Iterator<Entry<String, String>> it = attributeSet.iterator();
+			while(it.hasNext()) {
+				Entry<String,String> entry = it.next();
+				tagOutput.append(' ');
+				tagOutput.append(entry.getKey());
+				tagOutput.append("=\"");
+				tagOutput.append(entry.getValue());
+				tagOutput.append('"');
+			}
+		}
+		if (type==EMPTY) {
+			tagOutput.append('/');
+		}
+		tagOutput.append('>');
+		return tagOutput.toString();
+	}
+
+	public Hashtable<String, String> getAttributes() {
+		return this.attributes;
+	}
+}

src/de/gultsch/chat/xml/TagWriter.java 🔗

@@ -0,0 +1,46 @@
+package de.gultsch.chat.xml;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+import android.util.Log;
+
+public class TagWriter {
+	
+	OutputStreamWriter writer;
+	
+	public TagWriter() {
+		
+	}
+	
+	public TagWriter(OutputStream out) {
+		this.setOutputStream(out);
+	}
+	
+	public void setOutputStream(OutputStream out) {
+		this.writer = new OutputStreamWriter(out);
+	}
+	
+	public TagWriter beginDocument() throws IOException {
+		writer.write("<?xml version='1.0'?>");
+		return this;
+	}
+	
+	public TagWriter writeTag(Tag tag) throws IOException {
+		writer.write(tag.toString());
+		return this;
+	}
+	
+	public void flush() throws IOException {
+		writer.flush();
+	}
+
+	public void writeString(String string) throws IOException {
+		writer.write(string);
+	}
+
+	public void writeElement(Element element) throws IOException {
+		writer.write(element.toString());
+	}
+}

src/de/gultsch/chat/xml/XmlReader.java 🔗

@@ -0,0 +1,91 @@
+package de.gultsch.chat.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.Xml;
+
+public class XmlReader {
+	private static final String LOGTAG = "xmppService";
+	private XmlPullParser parser;
+	private PowerManager.WakeLock wakeLock;
+	private InputStream is;
+
+	public XmlReader(WakeLock wakeLock) {
+		this.parser = Xml.newPullParser();
+		try {
+			this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,true);
+		} catch (XmlPullParserException e) {
+			Log.d(LOGTAG,"error setting namespace feature on parser");
+		}
+		this.wakeLock = wakeLock;
+	}
+	
+	public void setInputStream(InputStream inputStream) {
+		this.is = inputStream;
+		try {
+			parser.setInput(new InputStreamReader(this.is));
+		} catch (XmlPullParserException e) {
+			Log.d(LOGTAG,"error setting input stream");
+		}
+	}
+	
+	public void reset() {
+		try {
+			parser.setInput(new InputStreamReader(this.is));
+		} catch (XmlPullParserException e) {
+			Log.d(LOGTAG,"error resetting input stream");
+		}
+	}
+	
+	public Tag readTag() throws XmlPullParserException, IOException {
+		if (wakeLock.isHeld()) {
+			//Log.d(LOGTAG,"there was a wake lock. releasing it till next event");
+			wakeLock.release(); //release wake look while waiting on next parser event
+		}
+		while(parser.next() != XmlPullParser.END_DOCUMENT) {
+				//Log.d(LOGTAG,"found new event. acquiring wake lock");
+				wakeLock.acquire();
+				if (parser.getEventType() == XmlPullParser.START_TAG) {
+					Tag tag = Tag.start(parser.getName());
+					for(int i = 0; i < parser.getAttributeCount(); ++i) {
+						tag.setAttribute(parser.getAttributeName(i), parser.getAttributeValue(i));
+					}
+					return tag;
+				} else if (parser.getEventType() == XmlPullParser.END_TAG) {
+					Tag tag = Tag.end(parser.getName());
+					return tag;
+				} else if (parser.getEventType() == XmlPullParser.TEXT) {
+					Tag tag = Tag.no(parser.getText());
+					return tag;
+				}
+			}
+		if (wakeLock.isHeld()) {
+			wakeLock.release();
+		}
+		return null; //end document;
+	}
+
+	public Element readElement(Tag currentTag) throws XmlPullParserException, IOException {
+		Element element = new Element(currentTag.getName());
+		element.setAttributes(currentTag.getAttributes());
+		Tag nextTag = this.readTag();
+		if(nextTag.isNo()) {
+			element.setContent(nextTag.getName());
+			nextTag = this.readTag();
+		}
+		while(!nextTag.isEnd(element.getName())) {
+			Element child = this.readElement(nextTag);
+			element.addChild(child);
+			nextTag = this.readTag();
+		}
+		return element;
+	}
+}

src/de/gultsch/chat/xmpp/IqPacket.java 🔗

@@ -0,0 +1,26 @@
+package de.gultsch.chat.xmpp;
+
+import de.gultsch.chat.xml.Element;
+
+public class IqPacket extends Element {
+	
+	public static final int TYPE_SET = 0;
+	public static final int TYPE_RESULT = 1;
+
+	private IqPacket(String name) {
+		super(name);
+	}
+
+	public IqPacket(String id, int type) {
+		super("iq");
+		this.setAttribute("id",id);
+		switch (type) {
+		case TYPE_SET:
+			this.setAttribute("type", "set");
+			break;
+		default:
+			break;
+		}
+	}
+
+}

src/de/gultsch/chat/xmpp/XmppConnection.java 🔗

@@ -0,0 +1,222 @@
+package de.gultsch.chat.xmpp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.os.PowerManager;
+import android.util.Log;
+import de.gultsch.chat.entities.Account;
+import de.gultsch.chat.utils.SASL;
+import de.gultsch.chat.xml.Element;
+import de.gultsch.chat.xml.Tag;
+import de.gultsch.chat.xml.XmlReader;
+import de.gultsch.chat.xml.TagWriter;
+
+public class XmppConnection implements Runnable {
+
+	protected Account account;
+	private static final String LOGTAG = "xmppService";
+
+	private PowerManager.WakeLock wakeLock;
+
+	private SecureRandom random = new SecureRandom();
+	
+	private Socket socket;
+	private XmlReader tagReader;
+	private TagWriter tagWriter;
+
+	private boolean isTlsEncrypted = false;
+	private boolean isAuthenticated = false;
+
+	public XmppConnection(Account account, PowerManager pm) {
+		this.account = account;
+		wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+				"XmppConnection");
+		tagReader = new XmlReader(wakeLock);
+		tagWriter = new TagWriter();
+	}
+
+	protected void connect() {
+		try {
+			socket = new Socket(account.getServer(), 5222);
+			Log.d(LOGTAG, "starting new socket");
+			OutputStream out = socket.getOutputStream();
+			tagWriter.setOutputStream(out);
+			InputStream in = socket.getInputStream();
+			tagReader.setInputStream(in);
+		} catch (UnknownHostException e) {
+			Log.d(LOGTAG, "error during connect. unknown host");
+		} catch (IOException e) {
+			Log.d(LOGTAG, "error during connect. io exception. falscher port?");
+		}
+	}
+
+	@Override
+	public void run() {
+		connect();
+		try {
+			tagWriter.beginDocument();
+			sendStartStream();
+			Tag nextTag;
+			while ((nextTag = tagReader.readTag()) != null) {
+				if (nextTag.isStart("stream")) {
+					processStream(nextTag);
+				} else {
+					Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
+				}
+			}
+		} catch (XmlPullParserException e) {
+			Log.d(LOGTAG,
+					"xml error during normal read. maybe missformed xml? "
+							+ e.getMessage());
+		} catch (IOException e) {
+			Log.d(LOGTAG, "io exception during read. connection lost?");
+		}
+	}
+
+	private void processStream(Tag currentTag) throws XmlPullParserException,
+			IOException {
+		Log.d(LOGTAG, "process Stream");
+		Tag nextTag;
+		while ((nextTag = tagReader.readTag()) != null) {
+			if (nextTag.isStart("error")) {
+				processStreamError(nextTag);
+			} else if (nextTag.isStart("features")) {
+				processStreamFeatures(nextTag);
+				if (!isTlsEncrypted) {
+					sendStartTLS();
+				}
+				if ((!isAuthenticated) && (isTlsEncrypted)) {
+					sendSaslAuth();
+				}
+				if ((isAuthenticated)&&(isTlsEncrypted)) {
+					sendBindRequest();
+				}
+			} else if (nextTag.isStart("proceed")) {
+				switchOverToTls(nextTag);
+			} else if (nextTag.isStart("success")) {
+				isAuthenticated = true;
+				Log.d(LOGTAG,"read success tag in stream. reset again");
+				tagReader.readTag();
+				tagReader.reset();
+				sendStartStream();
+				processStream(tagReader.readTag());
+			} else if (nextTag.isStart("iq")) {
+				processIq(nextTag);
+			} else if (nextTag.isEnd("stream")) {
+				break;
+			} else {
+				Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()
+						+ " as child of " + currentTag.getName());
+			}
+		}
+	}
+
+	private void processIq(Tag currentTag) throws XmlPullParserException, IOException {
+		int typ = -1;
+		if (currentTag.getAttribute("type").equals("result")) {
+			typ = IqPacket.TYPE_RESULT;
+		}
+		IqPacket iq = new IqPacket(currentTag.getAttribute("id"),typ);
+		Tag nextTag = tagReader.readTag();
+		while(!nextTag.isEnd("iq")) {
+			Element element = tagReader.readElement(nextTag);
+			iq.addChild(element);
+			nextTag = tagReader.readTag();
+		}
+		Log.d(LOGTAG,"this is what i understood: "+iq.toString());
+	}
+
+	private void sendStartTLS() throws XmlPullParserException, IOException {
+		Tag startTLS = Tag.empty("starttls");
+		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
+		Log.d(LOGTAG, "sending starttls");
+		tagWriter.writeTag(startTLS).flush();
+	}
+
+	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
+			IOException {
+		Tag nextTag = tagReader.readTag(); // should be proceed end tag
+		Log.d(LOGTAG, "now switch to ssl");
+		SSLSocket sslSocket;
+		try {
+			sslSocket = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory
+					.getDefault()).createSocket(socket, socket.getInetAddress()
+					.getHostAddress(), socket.getPort(), true);
+			tagReader.setInputStream(sslSocket.getInputStream());
+			Log.d(LOGTAG, "reset inputstream");
+			tagWriter.setOutputStream(sslSocket.getOutputStream());
+			Log.d(LOGTAG, "switch over seemed to work");
+			isTlsEncrypted = true;
+			sendStartStream();
+			processStream(tagReader.readTag());
+		} catch (IOException e) {
+			Log.d(LOGTAG, "error on ssl" + e.getMessage());
+		}
+	}
+
+	private void sendSaslAuth() throws IOException, XmlPullParserException {
+		String saslString = SASL.plain(account.getUsername(),
+				account.getPassword());
+		Element auth = new Element("auth");
+		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+		auth.setAttribute("mechanism", "PLAIN");
+		auth.setContent(saslString);
+		Log.d(LOGTAG,"sending sasl "+auth.toString());
+		tagWriter.writeElement(auth);
+		tagWriter.flush();
+	}
+
+	private void processStreamFeatures(Tag currentTag)
+			throws XmlPullParserException, IOException {
+		Log.d(LOGTAG, "processStreamFeatures");
+		
+		Element streamFeatures = new Element("features");
+		
+		Tag nextTag = tagReader.readTag();
+		while(!nextTag.isEnd("features")) {
+			Element element = tagReader.readElement(nextTag);
+			streamFeatures.addChild(element);
+			nextTag = tagReader.readTag();
+		}	
+	}
+
+	private void sendBindRequest() throws IOException {
+		IqPacket iq = new IqPacket(nextRandomId(),IqPacket.TYPE_SET);
+		Element bind = new Element("bind");
+		bind.setAttribute("xmlns","urn:ietf:params:xml:ns:xmpp-bind");
+		iq.addChild(bind);
+		Log.d(LOGTAG,"sending bind request: "+iq.toString());
+		tagWriter.writeElement(iq);
+		tagWriter.flush();
+	}
+
+	private void processStreamError(Tag currentTag) {
+		Log.d(LOGTAG, "processStreamError");
+	}
+
+	private void sendStartStream() throws IOException {
+		Tag stream = Tag.start("stream");
+		stream.setAttribute("from", account.getJid());
+		stream.setAttribute("to", account.getServer());
+		stream.setAttribute("version", "1.0");
+		stream.setAttribute("xml:lang", "en");
+		stream.setAttribute("xmlns", "jabber:client");
+		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
+		tagWriter.writeTag(stream).flush();
+	}
+
+	private String nextRandomId() {
+		return new BigInteger(50, random).toString(32);
+	}
+}