diff --git a/.gitignore b/.gitignore
index 3e0e2af634..c9cdf333ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ gen/*
*.orig
.pydevproject
.settings
+*~
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000..616f0bc0b6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+
+all:
+ android update project --path . --target android-10 --name Xabber
+ ant release
+
diff --git a/project.properties b/project.properties
index 4d7ea1e41e..ddd0fc4187 100644
--- a/project.properties
+++ b/project.properties
@@ -10,4 +10,4 @@
# Indicates whether an apk should be generated for each density.
split.density=false
# Project target.
-target=android-9
+target=android-10
diff --git a/res/values/account_editor.xml b/res/values/account_editor.xml
index ff4b40cbbe..bf67ed1d0c 100644
--- a/res/values/account_editor.xml
+++ b/res/values/account_editor.xml
@@ -25,6 +25,8 @@
username for gmail.com or Google Apps domain
+ Telephone number (with prefix)
+
username for livejournal.com
username for qip.ru
@@ -39,6 +41,8 @@
If you don\'t have Google account you may create one at http://mail.google.com\nAlso you can use your_user_name@your_google_domain
+ You need an already existing WhatsApp account. TODO: Add references on how to create/get password
+
If you don\'t have Livejournal account you may create one at http://livejournal.com
If you don\'t have QIP account you may create one at http://qip.ru
@@ -53,6 +57,8 @@
Google Talk
+ WhatsApp
+
LiveJournal
QIP
@@ -101,6 +107,8 @@
Google Talk
+ WhatsApp
+
WLM
XMPP
@@ -159,4 +167,4 @@
Are you sure you want to discard all the changes?
Incorrect user name. Check help text below for details.
-
\ No newline at end of file
+
diff --git a/res/values/connections.xml b/res/values/connections.xml
index 2e88520645..4e4042f6c1 100644
--- a/res/values/connections.xml
+++ b/res/values/connections.xml
@@ -16,6 +16,7 @@
xmpp
gtalk
wlm
+ wapp
+
+
+
+
+
+
+
+
+
diff --git a/src/com/xabber/android/data/account/AccountProtocol.java b/src/com/xabber/android/data/account/AccountProtocol.java
index caee5ba0d6..661af1d148 100644
--- a/src/com/xabber/android/data/account/AccountProtocol.java
+++ b/src/com/xabber/android/data/account/AccountProtocol.java
@@ -33,6 +33,11 @@ public enum AccountProtocol {
* GTalk.
*/
gtalk,
+
+ /**
+ * WhatsApp.
+ */
+ wapp,
/**
* Windows Live Messenger.
@@ -54,6 +59,8 @@ public int getNameResource() {
return R.string.account_type_names_xmpp;
else if (this == gtalk)
return R.string.account_type_names_gtalk;
+ else if (this == wapp)
+ return R.string.account_type_names_wapp;
else if (this == wlm)
return R.string.account_type_names_wlm;
else
@@ -68,6 +75,8 @@ public int getShortResource() {
return R.string.account_protocol_xmpp_title;
else if (this == gtalk)
return R.string.account_protocol_gtalk_title;
+ else if (this == wapp)
+ return R.string.account_protocol_wapp_title;
else if (this == wlm)
return R.string.account_protocol_wlm_title;
else
diff --git a/src/com/xabber/android/data/connection/ConnectionItem.java b/src/com/xabber/android/data/connection/ConnectionItem.java
index 4fbf4008b1..825b018452 100644
--- a/src/com/xabber/android/data/connection/ConnectionItem.java
+++ b/src/com/xabber/android/data/connection/ConnectionItem.java
@@ -14,7 +14,7 @@
*/
package com.xabber.android.data.connection;
-import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.Connection;
import com.xabber.android.data.Application;
import com.xabber.android.data.LogManager;
@@ -99,7 +99,7 @@ public String getRealJid() {
ConnectionThread connectionThread = getConnectionThread();
if (connectionThread == null)
return null;
- XMPPConnection xmppConnection = connectionThread.getXMPPConnection();
+ Connection xmppConnection = connectionThread.getXMPPConnection();
if (xmppConnection == null)
return null;
String user = xmppConnection.getUser();
@@ -188,7 +188,7 @@ protected void disconnect(final ConnectionThread connectionThread) {
Thread thread = new Thread("Disconnection thread for " + this) {
@Override
public void run() {
- XMPPConnection xmppConnection = connectionThread
+ Connection xmppConnection = connectionThread
.getXMPPConnection();
if (xmppConnection != null)
try {
@@ -261,7 +261,7 @@ protected void onAuthorized(ConnectionThread connectionThread) {
* @return true if connection thread was managed.
*/
private boolean onDisconnect(ConnectionThread connectionThread) {
- XMPPConnection xmppConnection = connectionThread.getXMPPConnection();
+ Connection xmppConnection = connectionThread.getXMPPConnection();
boolean acceptable = isManaged(connectionThread);
if (xmppConnection == null)
LogManager.i(this, "onClose " + acceptable);
diff --git a/src/com/xabber/android/data/connection/ConnectionManager.java b/src/com/xabber/android/data/connection/ConnectionManager.java
index 0d42d0179f..f74bf183b8 100644
--- a/src/com/xabber/android/data/connection/ConnectionManager.java
+++ b/src/com/xabber/android/data/connection/ConnectionManager.java
@@ -25,7 +25,7 @@
import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.SmackConfiguration;
-import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.packet.Packet;
@@ -181,7 +181,7 @@ public void sendPacket(String account, Packet packet)
|| !connectionThread.getConnectionItem().getState()
.isConnected())
throw new NetworkException(R.string.NOT_CONNECTED);
- XMPPConnection xmppConnection = connectionThread.getXMPPConnection();
+ Connection xmppConnection = connectionThread.getXMPPConnection();
try {
xmppConnection.sendPacket(packet);
} catch (IllegalStateException e) {
diff --git a/src/com/xabber/android/data/connection/ConnectionThread.java b/src/com/xabber/android/data/connection/ConnectionThread.java
index a57ce86d14..6cf16742bf 100644
--- a/src/com/xabber/android/data/connection/ConnectionThread.java
+++ b/src/com/xabber/android/data/connection/ConnectionThread.java
@@ -22,9 +22,12 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.jivesoftware.smack.WAConnection;
+
import javax.net.ssl.SSLException;
import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketFilter;
@@ -67,7 +70,7 @@ public class ConnectionThread implements
/**
* SMACK connection.
*/
- private XMPPConnection xmppConnection;
+ private Connection xmppConnection;
/**
* Thread holder for this connection.
@@ -144,7 +147,7 @@ public Thread newThread(Runnable runnable) {
started = false;
}
- public XMPPConnection getXMPPConnection() {
+ public Connection getXMPPConnection() {
return xmppConnection;
}
@@ -318,7 +321,13 @@ private void onReady(final InetAddress address, final int port) {
connectionConfiguration.setSecurityMode(tlsMode.getSecurityMode());
connectionConfiguration.setCompressionEnabled(compression);
- xmppConnection = new XMPPConnection(connectionConfiguration);
+ // Create different underlying classes depending on the protocol
+ AccountProtocol proto = connectionItem.getConnectionSettings().getProtocol();
+ if (proto == AccountProtocol.wapp) {
+ xmppConnection = new WAConnection(this, connectionConfiguration);
+ }else{
+ xmppConnection = new XMPPConnection(connectionConfiguration);
+ }
xmppConnection.addPacketListener(this, ACCEPT_ALL);
xmppConnection.forceAddConnectionListener(this);
connectionItem.onSRVResolved(this);
@@ -416,7 +425,8 @@ public void run() {
*/
private void connect(final String password) {
try {
- xmppConnection.connect();
+ ConnectionSettings connectionSettings = connectionItem.getConnectionSettings();
+ xmppConnection.connect(connectionSettings.getUserName(), password, connectionSettings.getResource());
} catch (XMPPException e) {
checkForCertificateError(e);
if (!checkForSeeOtherHost(e)) {
diff --git a/src/com/xabber/android/data/extension/attention/AttentionManager.java b/src/com/xabber/android/data/extension/attention/AttentionManager.java
index f995ba0c2e..2721759e36 100644
--- a/src/com/xabber/android/data/extension/attention/AttentionManager.java
+++ b/src/com/xabber/android/data/extension/attention/AttentionManager.java
@@ -18,7 +18,7 @@
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.ConnectionCreationListener;
-import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
@@ -112,7 +112,7 @@ public void onSettingsChanged() {
.getConnectionThread();
if (connectionThread == null)
continue;
- XMPPConnection xmppConnection = connectionThread
+ Connection xmppConnection = connectionThread
.getXMPPConnection();
if (xmppConnection == null)
continue;
diff --git a/src/com/xabber/android/data/extension/muc/MUCManager.java b/src/com/xabber/android/data/extension/muc/MUCManager.java
index 83065482fd..617382b23f 100644
--- a/src/com/xabber/android/data/extension/muc/MUCManager.java
+++ b/src/com/xabber/android/data/extension/muc/MUCManager.java
@@ -19,6 +19,7 @@
import java.util.Collections;
import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
@@ -129,7 +130,7 @@ private void onLoaded(Collection roomChats,
* @param room
* @return null if does not exists.
*/
- private RoomChat getRoomChat(String account, String room) {
+ public RoomChat getRoomChat(String account, String room) {
AbstractChat chat = MessageManager.getInstance().getChat(account, room);
if (chat != null && chat instanceof RoomChat)
return (RoomChat) chat;
@@ -219,19 +220,20 @@ public void run() {
* @param password
*/
public void createRoom(String account, String room, String nickname,
- String password, boolean join) {
+ String password, boolean join, String subject) {
removeInvite(getInvite(account, room));
AbstractChat chat = MessageManager.getInstance().getChat(account, room);
RoomChat roomChat;
if (chat == null || !(chat instanceof RoomChat)) {
if (chat != null)
MessageManager.getInstance().removeChat(chat);
- roomChat = new RoomChat(account, room, nickname, password);
+ roomChat = new RoomChat(account, room, nickname, password, subject);
MessageManager.getInstance().addChat(roomChat);
} else {
roomChat = (RoomChat) chat;
roomChat.setNickname(nickname);
roomChat.setPassword(password);
+ roomChat.setSubject(subject);
}
requestToWriteRoom(account, room, nickname, password, join);
if (join)
@@ -279,7 +281,7 @@ public boolean inUse(final String account, final String room) {
*/
public void joinRoom(final String account, final String room,
boolean requested) {
- final XMPPConnection xmppConnection;
+ final Connection xmppConnection;
final RoomChat roomChat;
final String nickname;
final String password;
diff --git a/src/com/xabber/android/data/extension/muc/RoomChat.java b/src/com/xabber/android/data/extension/muc/RoomChat.java
index 2825ec4cfc..7e3ca4c91d 100644
--- a/src/com/xabber/android/data/extension/muc/RoomChat.java
+++ b/src/com/xabber/android/data/extension/muc/RoomChat.java
@@ -82,6 +82,18 @@ public class RoomChat extends AbstractChat {
* Invited user for the sent packet ID.
*/
private final Map invites;
+
+ RoomChat(String account, String user, String nickname, String password, String subject) {
+ super(account, user);
+ this.nickname = nickname;
+ this.password = password;
+ requested = false;
+ state = RoomState.unavailable;
+ this.subject = subject;
+ multiUserChat = null;
+ occupants = new HashMap();
+ invites = new HashMap();
+ }
RoomChat(String account, String user, String nickname, String password) {
super(account, user);
@@ -156,6 +168,10 @@ String getSubject() {
return subject;
}
+ void setSubject(String s) {
+ this.subject = s;
+ }
+
MultiUserChat getMultiUserChat() {
return multiUserChat;
}
@@ -486,4 +502,4 @@ protected void onDisconnect() {
setState(RoomState.waiting);
}
-}
\ No newline at end of file
+}
diff --git a/src/com/xabber/android/ui/AccountEditor.java b/src/com/xabber/android/ui/AccountEditor.java
index 22b73c5314..fe6dc0be86 100644
--- a/src/com/xabber/android/ui/AccountEditor.java
+++ b/src/com/xabber/android/ui/AccountEditor.java
@@ -75,6 +75,8 @@ protected void onInflate(Bundle savedInstanceState) {
addPreferencesFromResource(R.xml.account_editor_xmpp);
else if (protocol == AccountProtocol.gtalk)
addPreferencesFromResource(R.xml.account_editor_xmpp);
+ else if (protocol == AccountProtocol.wapp)
+ addPreferencesFromResource(R.xml.account_editor_wapp);
else if (protocol == AccountProtocol.wlm)
addPreferencesFromResource(R.xml.account_editor_oauth);
else
diff --git a/src/com/xabber/android/ui/MUCEditor.java b/src/com/xabber/android/ui/MUCEditor.java
index 4c69378319..bb38f4cc62 100644
--- a/src/com/xabber/android/ui/MUCEditor.java
+++ b/src/com/xabber/android/ui/MUCEditor.java
@@ -206,7 +206,7 @@ public void onClick(View view) {
.removeMessageNotification(this.account, this.room);
}
MUCManager.getInstance()
- .createRoom(account, room, nick, password, join);
+ .createRoom(account, room, nick, password, join, "");
finish();
break;
default:
diff --git a/src/net/davidgf/android/DataBuffer.java b/src/net/davidgf/android/DataBuffer.java
new file mode 100644
index 0000000000..3f11f6de4d
--- /dev/null
+++ b/src/net/davidgf/android/DataBuffer.java
@@ -0,0 +1,286 @@
+
+/*
+ * JAVA WhatsApp API implementation
+ * Written by David Guillen Fandos (david@davidgf.net) based
+ * on the sources of WhatsAPI PHP implementation and whatsapp
+ * for libpurple.
+ * Updated to WA protocol v1.4
+ *
+ * Share and enjoy!
+ *
+ */
+
+package net.davidgf.android;
+
+import java.util.*;
+
+public class DataBuffer {
+ private byte [] buffer;
+
+ public DataBuffer (byte [] buf) {
+ buffer = new byte[buf.length];
+ for (int c = 0; c < buf.length; c++)
+ buffer[c] = buf[c];
+ }
+ public DataBuffer () {
+ buffer = new byte[0];
+ }
+ public DataBuffer (DataBuffer other) {
+ buffer = new byte[other.buffer.length];
+ for (int c = 0; c < other.buffer.length; c++)
+ buffer[c] = other.buffer[c];
+ }
+
+ DataBuffer addBuf(DataBuffer other) {
+ DataBuffer ret = new DataBuffer();
+ ret.buffer = new byte[this.buffer.length + other.buffer.length];
+
+ for (int c = 0; c < this.buffer.length; c++)
+ ret.buffer[c] = this.buffer[c];
+ for (int c = 0; c < other.buffer.length; c++)
+ ret.buffer[c+this.buffer.length] = other.buffer[c];
+
+ return ret;
+ }
+ DataBuffer decodedBuffer(RC4Decoder decoder, int clength) {
+ byte [] carray, array4;
+ carray = decoder.cipher(Arrays.copyOfRange(this.buffer,0,clength-4));
+ array4 = Arrays.copyOfRange(this.buffer,clength-4,clength);
+ DataBuffer deco = new DataBuffer(carray);
+ DataBuffer extra = new DataBuffer(array4);
+ return deco.addBuf(extra);
+ }
+ DataBuffer encodedBuffer(RC4Decoder decoder, byte [] key, boolean dout, int seq) {
+ DataBuffer deco = new DataBuffer(Arrays.copyOfRange(this.buffer,0,this.buffer.length));
+
+ deco.buffer = decoder.cipher(deco.buffer);
+ byte [] hmacint = KeyGenerator.calc_hmac(deco.buffer,key,seq);
+ DataBuffer hmac = new DataBuffer(hmacint);
+
+ DataBuffer res;
+ if (dout)
+ return deco.addBuf(hmac);
+ else
+ return hmac.addBuf(deco);
+ }
+ byte [] getPtr() {
+ return buffer;
+ }
+ void addData(byte [] dta) {
+ byte [] newbuf = new byte[buffer.length + dta.length];
+ for (int c = 0; c < buffer.length; c++)
+ newbuf[c] = buffer[c];
+ for (int c = 0; c < dta.length; c++)
+ newbuf[c+buffer.length] = dta[c];
+
+ this.buffer = newbuf;
+ }
+ void popData(int size) {
+ if (buffer.length >= size) {
+ buffer = Arrays.copyOfRange(buffer,size,buffer.length);
+ }
+ }
+ void crunchData(int size) {
+ if (buffer.length >= size) {
+ byte [] newbuf = new byte[buffer.length - size];
+ for (int c = 0; c < newbuf.length; c++)
+ newbuf[c] = buffer[c];
+
+ this.buffer = newbuf;
+ }
+ }
+ int getInt(int nbytes, int offset) {
+ //if (nbytes > blen)
+ // throw 0;
+ int ret = 0;
+ for (int i = 0; i < nbytes; i++) {
+ ret <<= 8;
+ ret |= (int)(buffer[i+offset] & 0xFF);
+ }
+ return ret;
+ }
+ void putInt(int value, int nbytes) {
+ //assert(nbytes > 0);
+
+ byte [] out = new byte[nbytes];
+ for (int i = 0; i < nbytes; i++) {
+ out[nbytes-i-1] = (byte)((value>>(i<<3)) & 0xFF);
+ }
+ this.addData(out);
+ }
+ int readInt(int nbytes) {
+ //if (nbytes > blen)
+ // throw 0;
+ int ret = getInt(nbytes,0);
+ popData(nbytes);
+ return ret;
+ }
+ int readListSize() {
+ //if (blen == 0)
+ // throw 0;
+ int ret;
+ if (buffer[0] == (byte)0xf8 || buffer[0] == (byte)0xf3) {
+ ret = (int)buffer[1];
+ popData(2);
+ }
+ else if (buffer[0] == (byte)0xf9) {
+ ret = getInt(2,1);
+ popData(3);
+ }
+ else {
+ // FIXME throw 0 error
+ ret = -1;
+ //printf("Parse error!!\n");
+ }
+ return ret;
+ }
+ void writeListSize(int size) {
+ if (size == 0) {
+ putInt(0,1);
+ }
+ else if (size < 256) {
+ putInt(0xf8,1);
+ putInt(size,1);
+ }
+ else {
+ putInt(0xf9,1);
+ putInt(size,2);
+ }
+ }
+ String readRawString(int size) {
+ //if (size < 0 or size > blen)
+ // throw 0;
+ String s = new String(buffer,0,size);
+ popData(size);
+ return s;
+ }
+ byte [] readRawByteString(int size) {
+ byte [] r = Arrays.copyOf(buffer,size);
+ popData(size);
+ return r;
+ }
+ byte [] readByteString() {
+ int type = readInt(1);
+ if (type > 2 && type <= 236) {
+ String ret = MiscUtil.getDecoded(type);
+ if (ret == null)
+ ret = MiscUtil.getDecodedExt(type, readInt(1));
+ return ret.getBytes();
+ }
+ else if (type == 252) {
+ int slen = readInt(1);
+ return readRawByteString(slen);
+ }
+ else if (type == 253) {
+ int slen = readInt(3);
+ return readRawByteString(slen);
+ }
+ else if (type == 250) {
+ String u = readString();
+ String s = readString();
+
+ if (u.length() > 0 && s.length() > 0)
+ return (u + "@" + s).getBytes();
+ else if (s.length() > 0)
+ return s.getBytes();
+ }
+ return new byte [0];
+ }
+ String readString() {
+ //if (blen == 0)
+ // throw 0;
+ int type = readInt(1);
+ if (type > 2 && type <= 236) {
+ String ret = MiscUtil.getDecoded(type);
+ if (ret == null)
+ ret = MiscUtil.getDecodedExt(type, readInt(1));
+ return ret;
+ }
+ else if (type == 0) {
+ return "";
+ }
+ else if (type == 252) {
+ int slen = readInt(1);
+ return readRawString(slen);
+ }
+ else if (type == 253) {
+ int slen = readInt(3);
+ return readRawString(slen);
+ }
+ else if (type == 250) {
+ String u = readString();
+ String s = readString();
+
+ if (u.length() > 0 && s.length() > 0)
+ return u + "@" + s;
+ else if (s.length() > 0)
+ return s;
+ return "";
+ }
+ return "";
+ }
+ void putRawString(byte [] s) {
+ if (s.length < 256) {
+ putInt(0xfc,1);
+ putInt(s.length,1);
+ addData(s);
+ }
+ else {
+ putInt(0xfd,1);
+ putInt(s.length,3);
+ addData(s);
+ }
+ }
+ void putString(String s) {
+ int lu = MiscUtil.lookupDecoded(s);
+ int sub_dict = (lu >> 8);
+
+ if (sub_dict != 0)
+ putInt(sub_dict + 236 - 1, 1); // Put dict byte first!
+
+ if (lu != 0) {
+ putInt(lu,1);
+ }
+ else if (s.indexOf('@') >= 0) {
+ String p1 = s.substring(0,s.indexOf('@'));
+ String p2 = s.substring(s.indexOf('@')+1);
+ putInt(0xfa,1);
+ putString(p1);
+ putString(p2);
+ }
+ else if (s.length() < 256) {
+ putInt(0xfc,1);
+ putInt(s.length(),1);
+ addData(s.getBytes());
+ }
+ else {
+ putInt(0xfd,1);
+ putInt(s.length(),3);
+ addData(s.getBytes());
+ }
+ }
+ boolean isList() {
+ //if (blen == 0)
+ // throw 0;
+ return (buffer[0] == (byte)248 || buffer[0] == (byte)0 || buffer[0] == (byte)249);
+ }
+ Vector readList(WhatsappConnection c) {
+ Vector l = new Vector();
+ int size = readListSize();
+ while (size-- > 0) {
+ l.add(c.read_tree(this));
+ }
+ return l;
+ }
+ int size() {
+ return buffer.length;
+ }
+ byte [] ToString() {
+ byte [] bb = new byte[buffer.length];
+ for (int c = 0; c < buffer.length; c++)
+ bb[c] = buffer[c];
+ return bb;
+ }
+};
+
+
diff --git a/src/net/davidgf/android/KeyGenerator.java b/src/net/davidgf/android/KeyGenerator.java
new file mode 100644
index 0000000000..37b0a136fc
--- /dev/null
+++ b/src/net/davidgf/android/KeyGenerator.java
@@ -0,0 +1,68 @@
+
+package net.davidgf.android;
+
+import java.security.*;
+import java.util.*;
+import javax.crypto.*;
+import javax.crypto.spec.*;
+
+public class KeyGenerator {
+
+ public static byte [] PKCS5_PBKDF2_HMAC_SHA1(byte [] pass, byte [] salt) throws Exception {
+ char [] password = new char[pass.length];
+ for (int i = 0; i < pass.length; i++)
+ password[i] = (char)(pass[i]&0xFF);
+
+ SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ PBEKeySpec ks = new PBEKeySpec(password,salt,2,20*8); // 2 rounds, 20*8 bits output
+ SecretKey s = f.generateSecret(ks);
+
+ return s.getEncoded();
+ }
+
+ private static byte [] hexmap = new byte[]{ '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' };
+
+ public static byte[][] generateKeyV14(String pw, byte [] salt) {
+ try {
+ byte [] decpass = MiscUtil.base64_decode(pw.getBytes());
+
+ byte [][] keys = new byte[4][];
+ for (int i = 0; i < 4; i++) {
+ byte salt2 [] = Arrays.copyOf(salt,salt.length + 1);
+ salt2[salt.length] = (byte)(i+1);
+ keys[i] = PKCS5_PBKDF2_HMAC_SHA1 (decpass,salt2);
+ }
+
+ return keys;
+ }
+ catch (Exception e) {
+ return new byte[0][0];
+ }
+ }
+ public static byte [] calc_hmac(byte [] data, byte [] key, int seq) {
+ byte [] dataext = Arrays.copyOf(data, data.length + 4);
+ dataext[data.length +0] = (byte)(seq >> 24);
+ dataext[data.length +1] = (byte)(seq >> 16);
+ dataext[data.length +2] = (byte)(seq >> 8);
+ dataext[data.length +3] = (byte)(seq );
+
+ byte [] hash = HMAC_SHA1 (dataext,key);
+
+ return Arrays.copyOf(hash,4);
+ }
+
+ private static byte [] HMAC_SHA1(byte [] text, byte [] key) {
+ try {
+ SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA1");
+ Mac mac = Mac.getInstance("HmacSHA1");
+ mac.init(signingKey);
+ // Compute the hmac on input data bytes
+ return mac.doFinal(text);
+ } catch (Exception e) {
+ return new byte[0];
+ }
+ }
+
+};
+
+
diff --git a/src/net/davidgf/android/MiscUtil.java b/src/net/davidgf/android/MiscUtil.java
new file mode 100644
index 0000000000..ab808a4659
--- /dev/null
+++ b/src/net/davidgf/android/MiscUtil.java
@@ -0,0 +1,232 @@
+
+package net.davidgf.android;
+
+import java.security.MessageDigest;
+import java.math.BigInteger;
+import java.util.*;
+
+public class MiscUtil {
+
+ private static byte [] base64_chars = new byte []{'A','B','C','D','E','F','G','H','I','J','K','L',
+ 'M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','i','j','k',
+ 'l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'};
+
+ private static boolean is_base64(byte cc) {
+ for (int c = 0; c < base64_chars.length; c++)
+ if (base64_chars[c] == cc)
+ return true;
+ return false;
+ }
+
+ private static byte findarray(byte what) {
+ for (int c = 0; c < base64_chars.length; c++)
+ if (base64_chars[c] == what)
+ return (byte)c;
+ return 0;
+ }
+
+ private static byte [] concat(byte [] array, byte c) {
+ byte [] bb = new byte[array.length+1];
+ for (int i = 0; i < array.length; i++)
+ bb[i] = array[i];
+
+ bb[array.length] = c;
+
+ return bb;
+ }
+
+ public static byte [] concat(byte [] array, byte [] array2) {
+ byte [] ret = Arrays.copyOf(array, array.length + array2.length);
+ System.arraycopy(array2,0, ret,ret.length, array2.length);
+ return ret;
+ }
+
+ public static byte [] base64_decode(byte [] encoded_string) {
+ int in_len = encoded_string.length;
+ int i = 0;
+ int j = 0;
+ int in_ = 0;
+ byte [] char_array_4 = new byte [4];
+ byte [] char_array_3 = new byte [3];
+ byte [] ret = new byte[0];
+
+ while (in_len-- > 0 && ( encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
+ char_array_4[i++] = encoded_string[in_]; in_++;
+ if (i ==4) {
+ for (i = 0; i <4; i++)
+ char_array_4[i] = findarray(char_array_4[i]);
+
+ char_array_3[0] = (byte)((char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >>> 4));
+ char_array_3[1] = (byte)(((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >>> 2));
+ char_array_3[2] = (byte)(((char_array_4[2] & 0x3) << 6) + char_array_4[3]);
+
+ for (i = 0; (i < 3); i++)
+ ret = concat(ret,char_array_3[i]);
+ i = 0;
+ }
+ }
+
+ if (i != 0) {
+ for (j = i; j <4; j++)
+ char_array_4[j] = 0;
+
+ for (j = 0; j <4; j++)
+ char_array_4[j] = findarray(char_array_4[j]);
+
+ char_array_3[0] = (byte)((char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >>> 4));
+ char_array_3[1] = (byte)(((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >>> 2));
+ char_array_3[2] = (byte)(((char_array_4[2] & 0x3) << 6) + char_array_4[3]);
+
+ for (j = 0; (j < i - 1); j++)
+ ret = concat(ret,char_array_3[j]);
+ }
+
+ return ret;
+ }
+
+
+ private static String [] dictionary = new String[] {
+ "", "", "", "account", "ack", "action", "active", "add", "after", "all", "allow", "apple",
+ "auth", "author", "available", "bad-protocol", "bad-request", "before", "body", "broadcast",
+ "cancel", "category", "challenge", "chat", "clean", "code", "composing", "config", "contacts",
+ "count", "create", "creation", "debug", "default", "delete", "delivery", "delta", "deny",
+ "digest", "dirty", "duplicate", "elapsed", "enable", "encoding", "error", "event",
+ "expiration", "expired", "fail", "failure", "false", "favorites", "feature", "features",
+ "feature-not-implemented", "field", "first", "free", "from", "g.us", "get", "google", "group",
+ "groups", "http://etherx.jabber.org/streams", "http://jabber.org/protocol/chatstates", "ib",
+ "id", "image", "img", "index", "internal-server-error", "ip", "iq", "item-not-found", "item",
+ "jabber:iq:last", "jabber:iq:privacy", "jabber:x:event", "jid", "kind", "last", "leave",
+ "list", "max", "mechanism", "media", "message_acks", "message", "method", "microsoft",
+ "missing", "modify", "mute", "name", "nokia", "none", "not-acceptable", "not-allowed",
+ "not-authorized", "notification", "notify", "off", "offline", "order", "owner", "owning",
+ "p_o", "p_t", "paid", "participant", "participants", "participating", "paused", "picture",
+ "pin", "ping", "platform", "port", "presence", "preview", "probe", "prop", "props", "query",
+ "raw", "read", "reason", "receipt", "received", "relay", "remote-server-timeout", "remove",
+ "request", "required", "resource-constraint", "resource", "response", "result", "retry",
+ "rim", "s_o", "s_t", "s.us", "s.whatsapp.net", "seconds", "server-error", "server",
+ "service-unavailable", "set", "show", "silent", "stat", "status", "stream:error",
+ "stream:features", "subject", "subscribe", "success", "sync", "t", "text", "timeout",
+ "timestamp", "to", "true", "type", "unavailable", "unsubscribe", "uri", "url",
+ "urn:ietf:params:xml:ns:xmpp-sasl", "urn:ietf:params:xml:ns:xmpp-stanzas",
+ "urn:ietf:params:xml:ns:xmpp-streams", "urn:xmpp:ping", "urn:xmpp:receipts",
+ "urn:xmpp:whatsapp:account", "urn:xmpp:whatsapp:dirty", "urn:xmpp:whatsapp:mms",
+ "urn:xmpp:whatsapp:push", "urn:xmpp:whatsapp", "user", "user-not-found", "value",
+ "version", "w:g", "w:p:r", "w:p", "w:profile:picture", "w", "wait", "WAUTH-2",
+ "x", "xmlns:stream", "xmlns", "1", "chatstate", "crypto", "enc", "class", "off_cnt",
+ "w:g2", "promote", "demote", "creator"
+ };
+
+ private static String [] dictionary2 = new String[] {
+ "Bell.caf", "Boing.caf", "Glass.caf", "Harp.caf", "TimePassing.caf", "Tri-tone.caf",
+ "Xylophone.caf", "background", "backoff", "chunked", "context", "full", "in", "interactive",
+ "out", "registration", "sid", "urn:xmpp:whatsapp:sync", "flt", "s16", "u8", "adpcm",
+ "amrnb", "amrwb", "mp3", "pcm", "qcelp", "wma", "h263", "h264", "jpeg", "mpeg4", "wmv",
+ "audio/3gpp", "audio/aac", "audio/amr", "audio/mp4", "audio/mpeg", "audio/ogg", "audio/qcelp",
+ "audio/wav", "audio/webm", "audio/x-caf", "audio/x-ms-wma", "image/gif", "image/jpeg",
+ "image/png", "video/3gpp", "video/avi", "video/mp4", "video/mpeg", "video/quicktime",
+ "video/x-flv", "video/x-ms-asf", "302", "400", "401", "402", "403", "404", "405", "406",
+ "407", "409", "500", "501", "503", "504", "abitrate", "acodec", "app_uptime", "asampfmt",
+ "asampfreq", "audio", "bb_db", "clear", "conflict", "conn_no_nna", "cost", "currency",
+ "duration", "extend", "file", "fps", "g_notify", "g_sound", "gcm", "google_play", "hash",
+ "height", "invalid", "jid-malformed", "latitude", "lc", "lg", "live", "location", "log",
+ "longitude", "max_groups", "max_participants", "max_subject", "mimetype", "mode",
+ "napi_version", "normalize", "orighash", "origin", "passive", "password", "played",
+ "policy-violation", "pop_mean_time", "pop_plus_minus", "price", "pricing", "redeem",
+ "Replaced by new connection", "resume", "signature", "size", "sound", "source",
+ "system-shutdown", "username", "vbitrate", "vcard", "vcodec", "video", "width",
+ "xml-not-well-formed", "checkmarks", "image_max_edge", "image_max_kbytes", "image_quality",
+ "ka", "ka_grow", "ka_shrink", "newmedia", "library", "caption", "forward", "c0", "c1", "c2",
+ "c3", "clock_skew", "cts", "k0", "k1", "login_rtt", "m_id", "nna_msg_rtt", "nna_no_off_count",
+ "nna_offline_ratio", "nna_push_rtt", "no_nna_con_count", "off_msg_rtt", "on_msg_rtt",
+ "stat_name", "sts", "suspect_conn", "lists", "self", "qr", "web", "w:b", "recipient",
+ "w:stats", "forbidden", "aurora.m4r", "bamboo.m4r", "chord.m4r", "circles.m4r", "complete.m4r",
+ "hello.m4r", "input.m4r", "keys.m4r", "note.m4r", "popcorn.m4r", "pulse.m4r", "synth.m4r",
+ "filehash"
+ };
+
+
+ public static String getDecoded(int n) {
+ if (n < 236)
+ return new String(dictionary[n & 255]);
+ return null;
+ }
+ public static String getDecodedExt(int n, int n2) {
+ if (n == 236)
+ return new String(dictionary2[n2 & 255]);
+ return "";
+ }
+ public static int lookupDecoded(String value) {
+ for (int i = 0; i < dictionary.length; i++) {
+ if (dictionary[i].equals(value))
+ return i;
+ }
+ for (int i = 0; i < dictionary2.length; i++) {
+ if (dictionary2[i].equals(value))
+ return i | 0x100;
+ }
+ return 0;
+ }
+
+ public static String bytesToUTF8(byte [] ba) {
+ try {
+ return new String(ba, "UTF-8");
+ }
+ catch (Exception e) {
+ return new String();
+ }
+ }
+
+ public static byte [] UTF8ToBytes(String st) {
+ try {
+ return st.getBytes("UTF-8");
+ }
+ catch (Exception e) {
+ return new byte [0];
+ }
+ }
+
+ public static String getUser(String user) {
+ return user.split("@")[0];
+ }
+
+ public static String getUserAndResource(String user) {
+ String [] re = user.split("/");
+ return user.split("@")[0] + "/" + re[re.length-1];
+ }
+
+ public static String getEncodedSha1Sum( byte [] data ) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA1");
+ md.update(data);
+ return new BigInteger(1, md.digest()).toString(16);
+ }
+ catch (Exception e) {
+ return new String("");
+ }
+ }
+
+ public static byte [] md5raw( byte [] data ) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ md.update(data);
+ return md.digest();
+ }
+ catch (Exception e) {
+ return new byte[0];
+ }
+ }
+ public static String md5hex( byte [] data ) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ md.update(data);
+ return new BigInteger(1, md.digest()).toString(16);
+ }
+ catch (Exception e) {
+ return new String("");
+ }
+ }
+
+
+}
+
+
diff --git a/src/net/davidgf/android/RC4Decoder.java b/src/net/davidgf/android/RC4Decoder.java
new file mode 100644
index 0000000000..6df280f213
--- /dev/null
+++ b/src/net/davidgf/android/RC4Decoder.java
@@ -0,0 +1,53 @@
+
+/*
+ * Java WhatsApp API implementation
+ * Written by David Guillen Fandos (david@davidgf.net) based
+ * on the sources of WhatsAPI PHP implementation and whatsapp
+ * for libpurple.
+ *
+ * Share and enjoy!
+ *
+ */
+
+package net.davidgf.android;
+
+public class RC4Decoder {
+ public int [] s;
+ public int i,j;
+ public void swap (int i, int j) {
+ int t = s[i];
+ s[i] = s[j];
+ s[j] = t;
+ }
+ public RC4Decoder(byte [] key, int drop) {
+ s = new int[256];
+ for (int k = 0; k < 256; k++) s[k] = k;
+ i = j = 0;
+ do {
+ int k = key[i % key.length] & 0xFF;
+ j = (j + k + s[i]) & 0xFF;
+ swap(i,j);
+ i = (i+1) & 0xFF;
+ } while (i != 0);
+ i = j = 0;
+
+ byte [] temp = new byte[drop];
+ for (int k = 0; k < drop; k++)
+ temp[k] = (byte)0;
+ cipher(temp);
+ }
+
+ public byte [] cipher (byte [] data) {
+ byte [] out = new byte[data.length];
+ for (int c = 0; c < data.length; c++) {
+ i = (i+1) & 0xFF;
+ j = (j + s[i]) & 0xFF;
+ swap(i,j);
+ int idx = (s[i]+s[j]) & 0xFF;
+ out[c] = (byte)((data[c] ^ s[idx]) & 0xFF);
+ }
+ return out;
+ }
+};
+
+
diff --git a/src/net/davidgf/android/Tree.java b/src/net/davidgf/android/Tree.java
new file mode 100644
index 0000000000..faddcb179e
--- /dev/null
+++ b/src/net/davidgf/android/Tree.java
@@ -0,0 +1,144 @@
+
+/*
+ * Java WhatsApp API implementation.
+ * Written by David Guillen Fandos (david@davidgf.net) based
+ * on the sources of WhatsAPI PHP implementation and whatsapp
+ * for libpurple.
+ *
+ * Share and enjoy!
+ *
+ */
+
+package net.davidgf.android;
+
+import java.util.*;
+
+public class Tree {
+ private Map < String, String > attributes;
+ private Vector < Tree > children;
+ private String tag;
+ private byte [] data;
+ private boolean forcedata;
+
+ public Tree(String tag) {
+ this.tag = tag;
+ this.forcedata=false;
+ this.data = new byte[0];
+ children = new Vector < Tree > ();
+ attributes = new HashMap < String, String > ();
+ }
+ public Tree(String tag, Map < String,String > attributes) {
+ this.tag = tag;
+ this.attributes = attributes;
+ this.forcedata = false;
+ this.data = new byte[0];
+ children = new Vector < Tree > ();
+ }
+ public void forceDataWrite() {
+ forcedata=true;
+ }
+ public boolean forcedData() {
+ return forcedata;
+ }
+ public void addChild(Tree t) {
+ children.add(t);
+ }
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+ public void setAttributes(Map < String, String > attributes) {
+ this.attributes = attributes;
+ }
+ public void readAttributes(DataBuffer data, int size) {
+ int count = (size - 2 + (size % 2)) / 2;
+ while (count-- > 0) {
+ String key = data.readString();
+ String value = data.readString();
+ attributes.put(key,value);
+ }
+ }
+ public void writeAttributes(DataBuffer data) {
+ for (String key: attributes.keySet()) {
+ data.putString(key);
+ data.putString(attributes.get(key));
+ }
+ }
+ public void setData(byte [] d) {
+ data = new byte[d.length];
+ for (int c = 0; c < d.length; c++)
+ data[c] = d[c];
+ }
+ public byte [] getData() {
+ byte [] b = new byte[data.length];
+ for (int c = 0; c < data.length; c++)
+ b[c] = data[c];
+ return b;
+ }
+ public String getTag() {
+ return tag;
+ }
+ public void setChildren(Vector < Tree > c) {
+ children = c;
+ }
+ public Vector < Tree > getChildren() {
+ return children;
+ }
+ public Map < String, String > getAttributes() {
+ return attributes;
+ }
+ public boolean hasAttributeValue(String at, String val) {
+ if (hasAttribute(at)) {
+ return (attributes.get(at).equals(val));
+ }
+ return false;
+ }
+ public boolean hasAttribute(String at) {
+ return (attributes.containsKey(at));
+ }
+ public String getAttribute(String at) {
+ if (attributes.containsKey(at))
+ return (attributes.get(at));
+ return "";
+ }
+
+ public Tree getChild(String tag) {
+ for (int i = 0; i < children.size(); i++) {
+ if (children.get(i).getTag().equals(tag))
+ return children.get(i);
+ Tree t = children.get(i).getChild(tag);
+ if (t != null)
+ return t;
+ }
+ return null;
+ }
+ public boolean hasChild(String tag) {
+ for (int i = 0; i < children.size(); i++) {
+ if (children.get(i).getTag().equals(tag))
+ return true;
+ if (children.get(i).hasChild(tag))
+ return true;
+ }
+ return false;
+ }
+
+ String toString(int sp) {
+ String ret = "";
+ String spacing = "";
+ for (int i = 0; i < sp*3; i++)
+ spacing += " ";
+ ret += spacing+"Tag: "+tag+"\n";
+ for (String key: attributes.keySet()) {
+ ret += spacing+"at["+key+"]="+attributes.get(key)+"\n";
+ }
+ ret += spacing+"Data: "+MiscUtil.bytesToUTF8(data)+"\n";
+
+ for (int i = 0; i < children.size(); i++) {
+ ret += children.get(i).toString(sp+1);
+ }
+ return ret;
+ }
+};
+
+
+
+
diff --git a/src/net/davidgf/android/WAConnection.java b/src/net/davidgf/android/WAConnection.java
new file mode 100644
index 0000000000..71fbd1ceee
--- /dev/null
+++ b/src/net/davidgf/android/WAConnection.java
@@ -0,0 +1,801 @@
+
+/*
+ * WhatsApp API extension for smack-xabber
+ * Written by David Guillen Fandos (david@davidgf.net) based
+ * on the sources of WhatsAPI PHP implementation and whatsapp
+ * for libpurple.
+ *
+ * Share and enjoy!
+ *
+ */
+
+package org.jivesoftware.smack;
+
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Registration;
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.packet.IQ;
+import com.xabber.xmpp.vcard.VCard;
+import com.xabber.xmpp.vcard.VCardProperty;
+import com.xabber.xmpp.vcard.BinaryPhoto;
+import com.xabber.android.data.connection.ConnectionThread;
+import com.xabber.android.data.account.AccountItem;
+import com.xabber.android.data.connection.ConnectionItem;
+
+import net.davidgf.android.WhatsappConnection;
+import net.davidgf.android.WAContacts;
+
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import java.io.*;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyStore;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * Creates a socket connection to a WA server.
+ *
+ * @see Connection
+ * @author David Guillen Fandos
+ */
+public class WAConnection extends Connection {
+
+ private ConnectionThread cthread;
+ /**
+ * The socket which is used for this connection.
+ */
+ protected Socket socket;
+
+ String connectionID = null;
+ private String user = null;
+ private boolean connected = false;
+ private boolean waconnected = false;
+ private String account_name = null;
+ /**
+ * Flag that indicates if the user is currently authenticated with the server.
+ */
+ private boolean authenticated = false;
+
+ private OutputStream ostream;
+ private InputStream istream;
+
+ private Thread writerThread, readerThread;
+
+ byte [] inbuffer;
+ byte [] outbuffer;
+ byte [] outbuffer_mutex;
+ WhatsappConnection waconnection;
+ Semaphore readwait,writewait;
+
+ private ExecutorService listenerExecutor;
+ int msgid;
+
+ private static final String waUA = "Android-2.11.151-443";
+
+ private String nickname = "";
+ Roster roster = null;
+
+ /**
+ * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be
+ * performed to determine the IP address and port corresponding to the
+ * service name; if that lookup fails, it's assumed that server resides at
+ * serviceName with the default port of 443.
+ * This is the simplest constructor for connecting to an WA server. Alternatively,
+ * you can get fine-grained control over connection settings using the
+ * {@link #WAConnection(ConnectionConfiguration)} constructor.
+ *
+ * Note that WAConnection constructors do not establish a connection to the server
+ * and you must call {@link #connect()}.
+ *
+ * The CallbackHandler is ignored.
+ *
+ * @param serviceName the name of the WA server to connect to; e.g. example.com.
+ * @param callbackHandler ignored and should be set to null
+ */
+ public WAConnection(ConnectionThread ct, String serviceName, CallbackHandler callbackHandler) {
+ // Create the configuration for this new connection
+ super(new ConnectionConfiguration(serviceName));
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ this.cthread = ct;
+ commonInit();
+ }
+
+ /**
+ * Creates a new WA connection in the same way {@link #WAConnection(String,CallbackHandler)} does, but
+ * with no callback handler for password prompting of the keystore.
+ *
+ * @param serviceName the name of the WA server to connect to; e.g. example.com.
+ */
+ public WAConnection(ConnectionThread ct, String serviceName) {
+ // Create the configuration for this new connection
+ super(new ConnectionConfiguration(serviceName));
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ this.cthread = ct;
+ commonInit();
+ }
+
+ /**
+ * Creates a new WA connection in the same way {@link #WAConnection(ConnectionConfiguration,CallbackHandler)} does, but
+ * with no callback handler for password prompting of the keystore.
+ *
+ * @param config the connection configuration.
+ */
+ public WAConnection(ConnectionThread ct, ConnectionConfiguration config) {
+ super(config);
+ this.cthread = ct;
+ commonInit();
+ }
+
+ public WAConnection(ConnectionThread ct, ConnectionConfiguration config, CallbackHandler callbackHandler) {
+ super(config);
+ config.setCallbackHandler(callbackHandler);
+ this.cthread = ct;
+ commonInit();
+ }
+
+ private void commonInit() {
+ outbuffer_mutex = new byte[1];
+ readwait = new Semaphore(0);
+ writewait = new Semaphore(0);
+
+ this.listenerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable,
+ "WA Listener Processor");
+ thread.setDaemon(true);
+ return thread;
+ }
+ });
+
+ ConnectionItem citem = cthread.getConnectionItem();
+ AccountItem aitem = ((AccountItem)citem);
+ account_name = aitem.getAccount();
+
+ nickname = aitem.getConnectionSettings().getResource();
+ }
+
+ public String getConnectionID() {
+ if (!isConnected()) {
+ return null;
+ }
+ return connectionID;
+ }
+
+ public String getUser() {
+ if (!isAuthenticated()) {
+ return null;
+ }
+ return user;
+ }
+
+ @Override
+ public synchronized void login(String username, String password, String resource) throws XMPPException {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ if (authenticated) {
+ throw new IllegalStateException("Already logged in to server.");
+ }
+ // Do partial version of nameprep on the username.
+ username = username.toLowerCase().trim();
+
+ this.user = username + "@" + getServiceName();
+ this.user += "/" + resource;
+
+ // Indicate that we're now authenticated.
+ authenticated = true;
+
+ if (config.isRosterLoadedAtLogin()) {
+ // Create the roster if it is not a reconnection or roster already
+ // created by getRoster()
+ if (this.roster == null) {
+ if (rosterStorage == null) {
+ this.roster = new Roster(this);
+ } else {
+ this.roster = new Roster(this, rosterStorage);
+ }
+ }
+ this.roster.reload();
+ }
+
+ // If debugging is enabled, change the the debug window title to include the
+ // name we are now logged-in as.
+ // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
+ // will be null
+ if (config.isDebuggerEnabled() && debugger != null) {
+ debugger.userHasLogged(user);
+ }
+
+ // Start login!
+ synchronized (waconnection) {
+ try {
+ waconnection.doLogin(waUA);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ popWriteData();
+ }
+
+ @Override
+ public synchronized void loginAnonymously() throws XMPPException {
+ // No anonymous login supported by WA
+ }
+
+ public Roster getRoster() {
+ // synchronize against login()
+ synchronized(this) {
+ // if connection is authenticated the roster is already set by login()
+ // or a previous call to getRoster()
+ if (!isAuthenticated()) {
+ if (roster == null) {
+ roster = new Roster(this);
+ }
+ return roster;
+ }
+ }
+
+ if (!config.isRosterLoadedAtLogin()) {
+ if (roster == null) {
+ roster = new Roster(this);
+ }
+ roster.reload();
+ }
+ // If this is the first time the user has asked for the roster after calling
+ // login, we want to wait for the server to send back the user's roster. This
+ // behavior shields API users from having to worry about the fact that roster
+ // operations are asynchronous, although they'll still have to listen for
+ // changes to the roster. Note: because of this waiting logic, internal
+ // Smack code should be wary about calling the getRoster method, and may need to
+ // access the roster object directly.
+ if (!roster.rosterInitialized) {
+ try {
+ synchronized (roster) {
+ long waitTime = SmackConfiguration.getPacketReplyTimeout();
+ long start = System.currentTimeMillis();
+ while (!roster.rosterInitialized) {
+ if (waitTime <= 0) {
+ break;
+ }
+ roster.wait(waitTime);
+ long now = System.currentTimeMillis();
+ waitTime -= now - start;
+ start = now;
+ }
+ }
+ }
+ catch (InterruptedException ie) {
+ // Ignore.
+ }
+ }
+ return roster;
+ }
+
+ /**
+ * Returns roster immediately without waiting for initialization.
+ *
+ * @return the user's roster, or null if the user has not logged in
+ * or has not received roster yet.
+ */
+ public Roster getRosterImmediately() {
+ return roster;
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ /**
+ *
+ * Returns true if the connection to the server is using legacy SSL or has successfully
+ * negotiated TLS. Once TLS has been negotiatied the connection has been secured. @see #isUsingTLS. @see #isUsingSSL.
+ *
+ * @return true if a secure connection to the server.
+ */
+ public boolean isSecureConnection() {
+ return false;
+ }
+
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ public boolean isAnonymous() {
+ return false;
+ }
+
+ /**
+ * Closes the connection by setting presence to unavailable then closing the stream to
+ * the XMPP server. The shutdown logic will be used during a planned disconnection or when
+ * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
+ * packet reader, packet writer, and {@link Roster} will not be removed; thus
+ * connection's state is kept.
+ *
+ * @param unavailablePresence the presence packet to send during shutdown.
+ */
+ protected void shutdown(Presence unavailablePresence) {
+ // Close connection
+ istream = null;
+ ostream = null;
+ writerThread = null;
+ readerThread = null;
+ outbuffer = null;
+ inbuffer = null;
+
+ // Set status
+ authenticated = false;
+ connected = false;
+ waconnected = false;
+
+ // Socket close
+ try {
+ if (socket != null)
+ socket.close();
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+ socket = null;
+ }
+
+ public void disconnect(Presence unavailablePresence) {
+ shutdown(unavailablePresence);
+
+ if (roster != null) {
+ roster.cleanup();
+ roster = null;
+ }
+ }
+
+ public void sendPacket(Packet packet) {
+ this.firePacketInterceptors(packet);
+
+ // If the packet if a Message, serialize and send it!
+ if (packet instanceof Message) {
+ Message m = (Message)packet;
+ synchronized (outbuffer_mutex) {
+ msgid++;
+ byte [] msg = waconnection.serializeMessage(m.getTo(), m.getBody(null), msgid);
+
+ // Put data in the output buffer
+ outbuffer = Arrays.copyOf(outbuffer, outbuffer.length + msg.length);
+ System.arraycopy(msg,0, outbuffer,outbuffer.length-msg.length, msg.length);
+ writewait.release();
+ }
+ }
+ if (packet instanceof Registration) {
+ Registration r = (Registration)packet;
+ System.out.println(r.getChildElementXML());
+ }
+ if (packet instanceof VCard) {
+ // The request for a VCard has been queued, now we should provide the Avatar!
+ packet.setFrom(packet.getTo());
+ VCard vc = (VCard)packet;
+
+ // Last seen at notes
+ vc.getProperties().put(VCardProperty.NOTE,waconnection.getNotes(packet.getFrom()));
+
+ BinaryPhoto bp = new BinaryPhoto();
+ bp.setData(waconnection.getUserAvatar(packet.getFrom()));
+ vc.getPhotos().add(bp);
+ submitPacket(packet);
+ }
+ if (packet instanceof Presence) {
+ // Sending our presence :)
+ String status = "unavailable";
+ Presence pres = (Presence)packet;
+ if (pres.getMode() == Presence.Mode.chat || pres.getMode() == Presence.Mode.available)
+ status = "available";
+
+ waconnection.setMyPresence(status, pres.getStatus());
+ }
+ if (packet instanceof RosterPacket) {
+ RosterPacket r = (RosterPacket)packet;
+ // Check Add/Remove Contact/Group:
+ if (r.getType() == IQ.Type.SET) {
+ Collection items = r.getRosterItems();
+ if (items.size() == 1) {
+ RosterPacket.Item it = ((RosterPacket.Item)(items.toArray()[0]));
+ if (it.getItemType() == RosterPacket.ItemType.remove) {
+ // Bypasss roster send/rcv for WA, as we do not store them in the server
+ submitPacket(r);
+ // Remove fromm storage
+ WAContacts.getInstance().removeContact(config.getUsername(), it.getUser());
+ }else{
+ // Adding contact, notify underlying connection for status query
+ waconnection.addContact(it.getUser(),true);
+ // Bypasss roster send/rcv for WA, as we do not store them in the server
+ submitPacket(r);
+ // Add the contact to the storage
+ WAContacts.getInstance().addContact(config.getUsername(), it.getUser(), it.getName());
+ }
+ }
+ }
+ // Query contacts!
+ if (r.getType() == IQ.Type.GET) {
+ RosterPacket rr = new RosterPacket();
+ rr.setType(IQ.Type.SET);
+
+ // Add saved contacts!
+ Vector < Vector < String > > saved_contacts = WAContacts.getInstance().getContacts(config.getUsername());
+ for (int i = 0; i < saved_contacts.size(); i++) {
+ String user = saved_contacts.get(i).get(0);
+ String name = saved_contacts.get(i).get(1);
+ rr.addRosterItem(new RosterPacket.Item(user, name));
+ // Subscribe presence
+ waconnection.addContact(user,true);
+ }
+
+ submitPacket(rr);
+ waconnection.pushGroupUpdate();
+ }
+
+ System.out.println(r.toXML());
+ }
+
+ // Notify others
+ this.firePacketSendingListeners(packet);
+
+ // DO stuff again
+ processLoop();
+ }
+
+ private void popWriteData() {
+ // Fill outbuffer with fresh data ready to be written
+ byte [] data;
+ synchronized (waconnection) {
+ data = waconnection.getWriteData();
+ }
+ synchronized (outbuffer_mutex) {
+ outbuffer = Arrays.copyOf(outbuffer, outbuffer.length + data.length);
+ System.arraycopy(data,0, outbuffer,outbuffer.length-data.length, data.length);
+ writewait.release();
+ }
+ }
+
+ /**
+ * Registers a packet interceptor with this connection. The interceptor will be
+ * invoked every time a packet is about to be sent by this connection. Interceptors
+ * may modify the packet to be sent. A packet filter determines which packets
+ * will be delivered to the interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to notify of packets about to be sent.
+ * @param packetFilter the packet filter to use.
+ * @deprecated replaced by {@link Connection#addPacketInterceptor(PacketInterceptor, PacketFilter)}.
+ */
+ public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor,
+ PacketFilter packetFilter) {
+ addPacketInterceptor(packetInterceptor, packetFilter);
+ }
+
+ /**
+ * Removes a packet interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to remove.
+ * @deprecated replaced by {@link Connection#removePacketInterceptor(PacketInterceptor)}.
+ */
+ public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) {
+ removePacketInterceptor(packetInterceptor);
+ }
+
+ /**
+ * Registers a packet listener with this connection. The listener will be
+ * notified of every packet that this connection sends. A packet filter determines
+ * which packets will be delivered to the listener. Note that the thread
+ * that writes packets will be used to invoke the listeners. Therefore, each
+ * packet listener should complete all operations quickly or use a different
+ * thread for processing.
+ *
+ * @param packetListener the packet listener to notify of sent packets.
+ * @param packetFilter the packet filter to use.
+ * @deprecated replaced by {@link #addPacketSendingListener(PacketListener, PacketFilter)}.
+ */
+ public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) {
+ addPacketSendingListener(packetListener, packetFilter);
+ }
+
+ /**
+ * Removes a packet listener for sending packets from this connection.
+ *
+ * @param packetListener the packet listener to remove.
+ * @deprecated replaced by {@link #removePacketSendingListener(PacketListener)}.
+ */
+ public void removePacketWriterListener(PacketListener packetListener) {
+ removePacketSendingListener(packetListener);
+ }
+
+ private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException {
+ String host = config.getHost();
+ int port = config.getPort();
+
+ // Create WA connection API object
+ // FIXME: Set proper nickname
+ msgid = 0;
+ waconnection = new WhatsappConnection(config.getUsername(), config.getPassword(), this.nickname, account_name);
+
+ try {
+ if (config.getSocketFactory() == null) {
+ this.socket = new Socket(host, port);
+ }
+ else {
+ this.socket = config.getSocketFactory().createSocket(host, port);
+ }
+ }
+ catch (UnknownHostException uhe) {
+ String errorMessage = "Could not connect to " + host + ":" + port + ".";
+ throw new XMPPException(errorMessage, new XMPPError(
+ XMPPError.Condition.remote_server_timeout, errorMessage),
+ uhe);
+ }
+ catch (IOException ioe) {
+ String errorMessage = "XMPPError connecting to " + host + ":"
+ + port + ".";
+ throw new XMPPException(errorMessage, new XMPPError(
+ XMPPError.Condition.remote_server_error, errorMessage), ioe);
+ }
+ System.out.println("Connected to " + host + " " + String.valueOf(port));
+ initConnection();
+ connected = true;
+ }
+
+ /**
+ * Initializes the connection by creating a packet reader and writer and opening a
+ * XMPP stream to the server.
+ *
+ * @throws XMPPException if establishing a connection to the server fails.
+ */
+ private void initConnection() throws XMPPException {
+ // Set the stream reader/writer
+ initReaderAndWriter();
+ outbuffer = new byte[0];
+ inbuffer = new byte[0];
+
+ // Spawn reader and writer threads
+ writerThread = new Thread() {
+ public void run() {
+ writePackets(this,ostream);
+ }
+ };
+ writerThread.setName("Socket data writer");
+ writerThread.setDaemon(true);
+ writerThread.start();
+
+ readerThread = new Thread() {
+ public void run() {
+ readPackets(this,istream);
+ }
+ };
+ readerThread.setName("Socket data reader");
+ readerThread.setDaemon(true);
+ readerThread.start();
+
+ // Make note of the fact that we're now connected.
+ connected = true;
+
+ for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
+ listener.connectionCreated(this);
+ }
+ }
+
+ private void initReaderAndWriter() throws XMPPException {
+ try {
+ istream = socket.getInputStream();
+ ostream = socket.getOutputStream();
+ }
+ catch (IOException ioe) {
+ throw new XMPPException(
+ "XMPPError establishing connection with server.",
+ new XMPPError(XMPPError.Condition.remote_server_error,
+ "XMPPError establishing connection with server."),
+ ioe);
+ }
+
+ //reader = new AliveReader(reader);
+ }
+
+ public boolean isUsingCompression() {
+ return false;
+ }
+
+ private void writePackets(Thread thisThread, OutputStream ostream) {
+ try {
+ while (outbuffer != null && ostream == this.ostream) {
+ if (outbuffer.length > 0) {
+ // Try to write the whole buffer
+ System.out.println("Writing packets...\n");
+ byte [] t;
+ synchronized (outbuffer_mutex) {
+ t = Arrays.copyOf(outbuffer,outbuffer.length);
+ }
+ ostream.write(t,0,t.length);
+ synchronized (outbuffer_mutex) {
+ // Pop the written data (the outbuffer may grow while writing t
+ outbuffer = Arrays.copyOfRange(outbuffer,t.length,outbuffer.length);
+ }
+ }
+ System.out.println("Sleeping while no packets are availbles...\n");
+ writewait.acquire();
+ }
+ }catch (Exception e) {
+ System.out.println("Error!\n" + e.toString());
+ }
+ System.out.println("Exiting writepackets thread (WA)\n");
+ }
+
+ private void submitPacket(Packet p) {
+ System.out.println("Received message!\n");
+ for (PacketCollector collector: getPacketCollectors()) {
+ collector.processPacket(p);
+ }
+ listenerExecutor.submit(new ListenerNotification(p));
+ }
+
+ private void readPackets(Thread thisThread, InputStream istream) {
+ try {
+ int r;
+ do {
+ System.out.println("Reading packets...\n");
+ byte [] buf = new byte[1024];
+ r = istream.read(buf,0,buf.length);
+ if (r > 0) {
+ synchronized (inbuffer) {
+ // Extend the array size and add the new bytes
+ inbuffer = Arrays.copyOf(inbuffer, inbuffer.length + r);
+ System.arraycopy(buf,0, inbuffer,inbuffer.length-r, r);
+ }
+ }
+
+ processLoop();
+ } while (r >= 0);
+ }catch (IOException e) {
+ System.out.println("Error!\n" + e.toString());
+ }
+ System.out.println("Exiting readpackets thread (WA)\n");
+ disconnect(new Presence(Presence.Type.unavailable));
+ cthread.connectionClosed();
+ // Signal the writer thread so it can also end
+ writewait.release();
+ }
+
+ private void processLoop() {
+ // Proceed to push data to underlying connection class
+ if (inbuffer == null) return;
+
+ synchronized ( waconnection ) {
+ synchronized (inbuffer) {
+ int used = waconnection.pushIncomingData(inbuffer);
+ inbuffer = Arrays.copyOfRange(inbuffer, used, inbuffer.length);
+ }
+ }
+
+ // Process stuff
+ this.popWriteData(); // Ready data might be waiting ...
+
+ if (waconnection.isConnected() && !waconnected) {
+ connectionOK(); // Notify the connection status
+ waconnected = true;
+ }
+
+ if (listenerExecutor == null) {
+ System.out.println("Null!!! Shit\n");
+ }
+
+ Packet p;
+ synchronized (waconnection) {
+ p = waconnection.getNextPacket();
+ }
+ while (p != null) {
+ submitPacket(p);
+ synchronized (waconnection) {
+ p = waconnection.getNextPacket();
+ }
+ }
+ }
+
+ /**
+ * Establishes a connection to the XMPP server and performs an automatic login
+ * only if the previous connection state was logged (authenticated). It basically
+ * creates and maintains a socket connection to the server.
+ *
+ * Listeners will be preserved from a previous connection if the reconnection
+ * occurs after an abrupt termination.
+ *
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ * Two possible errors can occur which will be wrapped by an XMPPException --
+ * UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+ * 502). The error codes and wrapped exceptions can be used to present more
+ * appropiate error messages to end-users.
+ */
+
+ // Specify pass at connect time
+ @Override
+ public void connect(final String user, final String pass, final String res) throws XMPPException {
+ config.setLoginInfo(user, pass, res);
+ connect();
+ }
+
+ public void connect() throws XMPPException {
+ // Stablishes the connection, readers and writers
+ connectUsingConfiguration(config);
+ // Automatically makes the login if the user was previouslly connected successfully
+ // to the server and the connection was terminated abruptly
+ }
+
+ public void connectionOK() {
+ for (ConnectionListener listener : getConnectionListeners()) {
+ try {
+ listener.reconnectionSuccessful();
+ }
+ catch (Exception e) {
+ // Catch and print any exception so we can recover
+ // from a faulty listener
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void setRosterStorage(RosterStorage storage)
+ throws IllegalStateException {
+ if(roster!=null){
+ throw new IllegalStateException("Roster is already initialized");
+ }
+ this.rosterStorage = storage;
+ }
+
+ /**
+ * Returns whether connection with server is alive.
+ *
+ * @return false if timeout occur.
+ */
+ @Override
+ public boolean isAlive() {
+ //PacketWriter packetWriter = this.packetWriter;
+ //return packetWriter == null || packetWriter.isAlive();
+ // FIXME
+ return true;
+ }
+
+
+ /**
+ * A runnable to notify all listeners of a packet.
+ */
+ private class ListenerNotification implements Runnable {
+
+ private Packet packet;
+
+ public ListenerNotification(Packet packet) {
+ this.packet = packet;
+ }
+
+ public void run() {
+ for (ListenerWrapper listenerWrapper : recvListeners.values()) {
+ try {
+ listenerWrapper.notifyListener(packet);
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+}
+
diff --git a/src/net/davidgf/android/WAContacts.java b/src/net/davidgf/android/WAContacts.java
new file mode 100644
index 0000000000..aa4349b8a8
--- /dev/null
+++ b/src/net/davidgf/android/WAContacts.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2013, Redsolution LTD. All rights reserved.
+ *
+ * This file is part of Xabber project; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License, Version 3.
+ *
+ * Xabber is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License,
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package net.davidgf.android;
+
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+
+import com.xabber.android.data.DatabaseManager;
+import com.xabber.android.data.entity.AbstractAccountTable;
+import java.util.*;
+
+/**
+ * Storage for whatsapp contacts
+ *
+ * @author david.guillen
+ */
+public class WAContacts extends AbstractAccountTable {
+
+ private static final class Fields implements AbstractAccountTable.Fields {
+
+ private Fields() {
+ }
+
+ public static final String ACCOUNT = "account";
+ public static final String USER_PHONE = "user_phone";
+ public static final String USER_NAME = "user_name";
+ }
+
+ private static final String NAME = "wa_contacts";
+ private static final String[] PROJECTION = new String[] { Fields.ACCOUNT, Fields.USER_PHONE, Fields.USER_NAME };
+ static final boolean DEFAULT_EXPANDED = true;
+
+ private final DatabaseManager databaseManager;
+ private SQLiteStatement writeStatement;
+ private final Object writeLock;
+
+ private final static WAContacts instance;
+
+ static {
+ instance = new WAContacts(DatabaseManager.getInstance());
+ DatabaseManager.getInstance().addTable(instance);
+ }
+
+ public static WAContacts getInstance() {
+ return instance;
+ }
+
+ private WAContacts(DatabaseManager databaseManager) {
+ this.databaseManager = databaseManager;
+ writeStatement = null;
+ writeLock = new Object();
+ }
+
+ @Override
+ public void create(SQLiteDatabase db) {
+ String sql = "CREATE TABLE IF NOT EXISTS " + NAME + " ("
+ + Fields.ACCOUNT + " TEXT,"
+ + Fields.USER_PHONE + " TEXT,"
+ + Fields.USER_NAME + " TEXT);";
+ DatabaseManager.execSQL(db, sql);
+ }
+
+ @Override
+ public void migrate(SQLiteDatabase db, int toVersion) {
+ super.migrate(db, toVersion);
+ // No migration at this time
+ }
+
+ public void addContact(String account, String user, String name) {
+ synchronized (writeLock) {
+ if (writeStatement == null) {
+ SQLiteDatabase db = databaseManager.getWritableDatabase();
+ create(db);
+ writeStatement = db.compileStatement("INSERT OR REPLACE INTO "
+ + NAME + " (" + Fields.ACCOUNT + ", "
+ + Fields.USER_PHONE + ", "
+ + Fields.USER_NAME + ") VALUES (?, ?, ?);");
+ }
+ writeStatement.bindString(1, account);
+ writeStatement.bindString(2, user);
+ writeStatement.bindString(3, name);
+ writeStatement.execute();
+ }
+ }
+
+ public void removeContact(String account, String user) {
+ synchronized (writeLock) {
+ if (writeStatement == null) {
+ SQLiteDatabase db = databaseManager.getWritableDatabase();
+ create(db);
+ writeStatement = db.compileStatement("DELETE FROM "
+ + NAME + " WHERE " + Fields.USER_PHONE + " = "
+ + "? AND " + Fields.ACCOUNT + " = ?;");
+ }
+ writeStatement.bindString(1, user);
+ writeStatement.bindString(2, account);
+ writeStatement.execute();
+ }
+ }
+
+ @Override
+ protected String getTableName() {
+ return NAME;
+ }
+
+ @Override
+ protected String[] getProjection() {
+ return PROJECTION;
+ }
+
+ public Vector < Vector > getContacts(String account) {
+ Vector < Vector > ret = new Vector < Vector >();
+
+ SQLiteDatabase db = databaseManager.getWritableDatabase();
+ create(db);
+ Cursor c = db.query(NAME, PROJECTION, "account = '" + account + "'", null, null, null, null);
+
+ if (c.moveToFirst()) {
+ do {
+ String user = c.getString(1);
+ String name = c.getString(2);
+ Vector < String > e = new Vector ();
+ e.add(user);
+ e.add(name);
+ ret.add(e);
+ } while(c.moveToNext());
+ }
+
+ return ret;
+ }
+
+}
+
diff --git a/src/net/davidgf/android/WhatsappConnection.java b/src/net/davidgf/android/WhatsappConnection.java
new file mode 100644
index 0000000000..121227306e
--- /dev/null
+++ b/src/net/davidgf/android/WhatsappConnection.java
@@ -0,0 +1,922 @@
+
+/*
+ * Java WhatsApp API implementation.
+ * Written by David Guillen Fandos (david@davidgf.net) based
+ * on the sources of WhatsAPI PHP implementation and whatsapp
+ * for libpurple.
+ *
+ * Share and enjoy!
+ *
+ */
+
+package net.davidgf.android;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.packet.MessageEvent;
+import org.jivesoftware.smackx.packet.Nick;
+import org.jivesoftware.smackx.packet.MUCUser;
+import org.jivesoftware.smackx.packet.DelayInformation;
+import org.jivesoftware.smackx.packet.MUCInitialPresence;
+import com.xabber.android.data.extension.muc.MUCManager;
+import com.xabber.xmpp.vcard.VCard;
+import com.xabber.xmpp.avatar.VCardUpdate;
+
+import java.net.*;
+import java.util.*;
+import java.io.*;
+
+public class WhatsappConnection {
+ private RC4Decoder in, out;
+ private byte session_key[][];
+ private DataBuffer outbuffer;
+ private byte []challenge_data;
+ private int frame_seq;
+
+ private enum SessionStatus { SessionNone, SessionConnecting, SessionWaitingChallenge, SessionWaitingAuthOK, SessionConnected };
+ private SessionStatus conn_status;
+
+ private String phone, password, nickname;
+ private static String whatsappserver = "s.whatsapp.net";
+ private static String whatsappservergroup = "g.us";
+
+ private String account_type, account_status, account_expiration, account_creation;
+ private String mypresence;
+
+ private Vector received_packets;
+ private Vector contacts;
+ private Map groups;
+ boolean groups_updated;
+ int gq_stat;
+ int gw1,gw2;
+
+ private int iqid;
+ private String mymessage = "";
+ private String account_name = null;
+
+ // HTTPS interface
+ private String sslnonce = "";
+ private Thread http_thread;
+
+ public WhatsappConnection(String phone, String pass, String nick, String aname) {
+ session_key = new byte[4][20];
+
+ this.phone = phone;
+ this.password = pass.trim();
+ this.conn_status = SessionStatus.SessionNone;
+ this.nickname = nick;
+ this.mypresence = "available";
+ outbuffer = new DataBuffer();
+ received_packets = new Vector ();
+ contacts = new Vector ();
+ iqid = 0;
+
+ groups = new HashMap ();
+ groups_updated = false;
+ gq_stat = 0;
+ gw1 = gw2 = 0;
+
+ account_name = aname;
+ }
+
+ public Tree read_tree(DataBuffer data) {
+ int lsize = data.readListSize();
+ int type = data.getInt(1,0);
+ if (type == 1) {
+ data.popData(1);
+ Tree t = new Tree("start");
+ t.readAttributes(data,lsize);
+ return t;
+ }else if (type == 2) {
+ data.popData(1);
+ return new Tree("treeerr"); // No data in this tree...
+ }
+
+ Tree t = new Tree(data.readString());
+ t.readAttributes(data,lsize);
+
+ if ((lsize & 1) == 1) {
+ return t;
+ }
+
+ if (data.isList()) {
+ t.setChildren(data.readList(this));
+ }else{
+ t.setData(data.readByteString());
+ }
+
+ return t;
+ }
+
+ Tree parse_tree(DataBuffer data) {
+ int bflag = (data.getInt(1,0) & 0xF0)>>4;
+ int bsize = data.getInt(2,1);
+ if (bsize > data.size()-3) {
+ return new Tree("treeerr"); // Next message incomplete, return consumed data
+ }
+ data.popData(3);
+
+ if ((bflag & 0x8) != 0) {
+ // Decode data, buffer conversion
+ if (this.in != null) {
+ DataBuffer decoded_data = data.decodedBuffer(this.in,bsize);
+
+ Tree tt = read_tree(decoded_data);
+
+ data.popData(bsize); // Pop data unencrypted for next parsing!
+
+ // Remove hash
+ decoded_data.popData(4);
+
+ return tt;
+ }else{
+ data.popData(bsize);
+ return new Tree("treeerr");
+ }
+ }else{
+ return read_tree(data);
+ }
+ }
+
+ public int pushIncomingData(byte [] data) {
+ if (data.length < 3) return 0;
+
+ DataBuffer db = new DataBuffer(data);
+
+ Tree t;
+ do {
+ t = this.parse_tree(db);
+ if (!t.getTag().equals("treeerr"))
+ this.processPacket(t);
+
+ System.out.println(t.toString(0));
+ } while (!t.getTag().equals("treeerr") && db.size() >= 3);
+
+ return data.length - db.size();
+ }
+
+ public boolean isConnected() {
+ return conn_status == SessionStatus.SessionConnected;
+ }
+
+ private void processPacket(Tree t) {
+ // Now process the tree list!
+ if (t.getTag().equals("challenge")) {
+ // Generate a session key using the challege & the password
+ assert(conn_status == SessionStatus.SessionWaitingChallenge);
+
+ // Update key generation to V1.4
+ session_key = KeyGenerator.generateKeyV14(password,t.getData());
+ System.out.println(password);
+
+ this.in = new RC4Decoder(session_key[2], 768);
+ this.out = new RC4Decoder(session_key[0], 768);
+
+ conn_status = SessionStatus.SessionWaitingAuthOK;
+ challenge_data = t.getData();
+
+ this.sendAuthResponse();
+ }
+ else if (t.getTag().equals("success")) {
+ // Notifies the success of the auth
+ conn_status = SessionStatus.SessionConnected;
+ if (t.hasAttribute("status"))
+ this.account_status = t.getAttributes().get("status");
+ if (t.hasAttribute("kind"))
+ this.account_type = t.getAttributes().get("kind");
+ if (t.hasAttribute("expiration"))
+ this.account_expiration = t.getAttributes().get("expiration");
+ if (t.hasAttribute("creation"))
+ this.account_creation = t.getAttributes().get("creation");
+
+ this.notifyMyPresence();
+ //this->sendInitial();
+ this.updateGroups();
+
+ // Resend contact status query (for already added contacts)
+ for (int i = 0; i < contacts.size(); i++) {
+ subscribePresence(contacts.get(i).phone);
+ queryPreview(contacts.get(i).phone);
+ getLastSeen(contacts.get(i).phone);
+ }
+ }
+ else if (t.getTag().equals("notification")) {
+ DataBuffer reply = generateResponse(t.getAttribute("from"),
+ t.getAttribute("type"),
+ t.getAttribute("id"));
+ outbuffer = outbuffer.addBuf(reply);
+ }
+ else if (t.getTag().equals("presence")) {
+ // Receives the presence of the user
+ if ( t.hasAttribute("from") ) {
+ Presence.Mode mode = Presence.Mode.available;
+ if (t.hasAttribute("type"))
+ if (t.getAttribute("type").equals("unavailable"))
+ mode = Presence.Mode.away;
+
+ Presence presp = new Presence(Presence.Type.available);
+ presp.setMode(mode);
+ presp.setFrom(MiscUtil.getUser(t.getAttribute("from")));
+ presp.setTo(MiscUtil.getUser(phone));
+ received_packets.add(presp);
+
+ // Schedule the last seen querying
+ getLastSeen(t.getAttribute("from"));
+ }
+ }
+ else if (t.getTag().equals("iq")) {
+ // PING
+ if (t.hasAttribute("from") && t.hasAttribute("id") && t.hasChild("urn:xmpp:ping")) {
+ this.doPong(t.getAttribute("id"),t.getAttribute("from"));
+ }
+
+ // Preview query
+ if (t.hasAttributeValue("type","result") && t.hasAttribute("from")) {
+ Tree tb = t.getChild("picture");
+ if (tb != null) {
+ if (tb.hasAttributeValue("type","preview"))
+ this.addPreviewPicture(t.getAttribute("from"),tb.getData());
+ }
+ tb = t.getChild("query");
+ if (tb != null) {
+ if (tb.hasAttribute("seconds")) {
+ this.notifyLastSeen(t.getAttribute("from"),tb.getAttribute("seconds"));
+ }
+ }
+ }
+
+ // Status message
+ if (t.hasChild("status")) {
+ Vector childs = t.getChildren();
+ for (int j = 0; j < childs.size(); j++) {
+ if (childs.get(j).getTag().equals("user"))
+ this.notifyStatus(childs.get(j).getAttribute("jid"),MiscUtil.bytesToUTF8(childs.get(j).getData()));
+ }
+ }
+
+ // Group stuff
+ Vector childs = t.getChildren();
+ int acc = 0;
+ for (int j = 0; j < childs.size(); j++) {
+ if (childs.get(j).getTag().equals("group")) {
+ boolean rep = groups.containsKey(MiscUtil.getUser(childs.get(j).getAttribute("id")));
+ if (!rep) {
+ groups.put(
+ MiscUtil.getUser(childs.get(j).getAttribute("id")),
+ new Group( MiscUtil.getUser(childs.get(j).getAttribute("id")),
+ childs.get(j).getAttribute("subject"),
+ MiscUtil.getUser(childs.get(j).getAttribute("owner")) ) );
+
+ // Query group participants
+ final String iid = String.valueOf(++iqid);
+ final String pid = childs.get(j).getAttribute("id");
+ final String subj = childs.get(j).getAttribute("subject");
+ Tree iq = new Tree("list");
+ Tree req = new Tree("iq",
+ new HashMap < String,String >(){{
+ put("id",iid);
+ put("type","get");
+ put("to",pid+"@g.us");
+ put("xmlns","w:g");
+ }});
+ req.addChild(iq);
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true)));
+
+ // Add group as a contact
+ pushGroupUpdate();
+ }
+ }
+ else if (childs.get(j).getTag().equals("participant")) {
+ String gid = MiscUtil.getUser(t.getAttribute("from"));
+ String pt = MiscUtil.getUser(childs.get(j).getAttribute("jid"));
+ if (groups.containsKey(gid)) {
+ groups.get(gid).participants.add(pt);
+
+ pushGroupUpdate();
+ }
+ }
+ }
+
+ Tree tb = t.getChild("group");
+ if (tb != null) {
+ if (tb.hasAttributeValue("type","preview"))
+ this.addPreviewPicture(t.getAttribute("from"),t.getData());
+ }
+ }
+ else if (t.getTag().equals("message")) {
+ if (t.hasAttribute("from") &&
+ (t.hasAttributeValue("type","text") || t.hasAttributeValue("type","media")) ) {
+ long time = 0;
+ if (t.hasAttribute("t"))
+ time = Integer.parseInt(t.getAttribute("t"));
+ String from = t.getAttribute("from");
+ String id = t.getAttribute("id");
+ String author = t.getAttribute("participant");
+
+ // Group nickname
+ if (from.contains("@g.us") && t.hasChild("notify")) {
+ author = t.getChild("notify").getAttribute("name");
+ from = from + "/" + author;
+ }
+
+ Tree tb = t.getChild("body");
+ if (tb != null) {
+ // TODO: Add user here and at UI in case we don't have it
+ this.receiveMessage(
+ new ChatMessage(from,time,id,MiscUtil.bytesToUTF8(tb.getData()),author));
+ addContact(from,false);
+ }
+
+ tb = t.getChild("media");
+ if (tb != null) {
+ // Photo/audio
+ if (tb.hasAttributeValue("type","image") ||
+ tb.hasAttributeValue("type","audio") ||
+ tb.hasAttributeValue("type","video")) {
+
+ this.receiveMessage(
+ new ImageMessage(from,time,id,tb.getAttribute("url"),tb.getData(),author));
+ addContact(from,false);
+ }
+ }
+ }
+ else if (t.hasAttributeValue("type", "notification") && t.hasAttribute("from")) {
+ /* If the nofitication comes from a group, assume we have to reload groups ;) */
+ updateGroups();
+ }
+
+ // Received ACK
+ if (t.hasAttribute("type") && t.hasAttribute("from")) {
+ DataBuffer reply = generateResponse(t.getAttribute("from"),
+ t.getAttribute("type"),
+ t.getAttribute("id"));
+ outbuffer = outbuffer.addBuf(reply);
+
+ }
+ }
+ else if (t.getTag().equals("chatstate")) {
+ if (t.hasChild("composing")) {
+ gotTyping(t.getAttribute("from"),true);
+ }
+ if (t.hasChild("paused")) {
+ gotTyping(t.getAttribute("from"),false);
+ }
+ }
+ else if (t.getTag().equals("receipt")) {
+ final String pid = t.getAttribute("id");
+ final String typed = t.getAttribute("type");
+ final String typef = (typed.equals("") ? "delivery" : typed);
+ Tree req = new Tree("ack",
+ new HashMap < String,String >(){{
+ put("id",pid);
+ put("type",typef);
+ put("class","receipt");
+ }});
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true)));
+ }
+
+
+ /*else if (treelist[i].getTag() == "failure") {
+ if (conn_status == SessionWaitingAuthOK)
+ this->notifyError(errorAuth);
+ else
+ this->notifyError(errorUnknown);
+ }*/
+ }
+
+ private String last_seen_text(long t) {
+ if (t < 60)
+ return String.valueOf(t) + " seconds ago";
+ else if (t < 60*60)
+ return String.valueOf(t/60) + " minute(s) ago";
+ else if (t < 60*60*24)
+ return String.valueOf(t/60/60) + " hour(s) ago";
+ else if (t < 48*60*60)
+ return "yesterday";
+ else
+ return String.valueOf(t/60/60/24) + " day(s) ago";
+ }
+
+ private void notifyLastSeen(final String from, final String seconds) {
+ final String u = MiscUtil.getUser(from);
+ final long sec = Integer.parseInt(seconds);
+
+ // Save last seen time
+ for (int i = 0; i < contacts.size(); i++) {
+ if (contacts.get(i).phone.equals(u)) {
+ contacts.get(i).last_seen = sec;
+ break;
+ }
+ }
+
+ requestVCardUpdate(u);
+ }
+
+ private void notifyStatus(final String from, final String status) {
+ final String u = MiscUtil.getUser(from);
+
+ // Save last seen time
+ for (int i = 0; i < contacts.size(); i++) {
+ if (contacts.get(i).phone.equals(u)) {
+ contacts.get(i).status = status;
+ break;
+ }
+ }
+
+ requestVCardUpdate(u);
+ }
+
+ public String getNotes(String u) {
+ u = MiscUtil.getUser(u);
+
+ String note = "";
+ for (int i = 0; i < contacts.size(); i++) {
+ if (contacts.get(i).phone.equals(u)) {
+ note = "Last seen: " + last_seen_text(contacts.get(i).last_seen);
+ break;
+ }
+ }
+
+ return note;
+ }
+
+ public void pushGroupUpdate() {
+ for (Map.Entry entry : groups.entrySet()) {
+ // Create room
+ MUCManager.getInstance().createRoom(
+ account_name, entry.getValue().id, phone, "", false, entry.getValue().subject);
+
+ for (int i = 0; i < entry.getValue().participants.size(); i++) {
+ // Send the room status (chek MultiUserChat.java),matched the filter
+ Presence.Mode mode = Presence.Mode.chat;
+ Presence presp = new Presence(Presence.Type.available);
+ presp.setMode(mode);
+ presp.setFrom(entry.getValue().id + "/" + entry.getValue().participants.get(i));
+ presp.setTo(MiscUtil.getUser(phone));
+ // Add MUC User
+ MUCUser user = new MUCUser();
+ user.setItem(new MUCUser.Item("member","participant"));
+ presp.addExtension(user);
+ received_packets.add(presp);
+ }
+ }
+ }
+
+ private DataBuffer generateResponse(final String from, final String type, final String id) {
+ Tree mes = new Tree("receipt",new HashMap < String,String >() {{
+ put("to",from); put("id",id); }} );
+ return serialize_tree(mes,true);
+ }
+
+ private void queryPreview(final String user) {
+ final String fuser = user+"@"+whatsappserver;
+ final String reqid = String.valueOf(++iqid);
+ Tree pic = new Tree ("picture",
+ new HashMap < String,String >() {{ put("type","preview"); }} );
+ Tree req = new Tree("iq",
+ new HashMap < String,String >() {{
+ put("id",reqid);
+ put("type","get");
+ put("to",fuser);
+ put("xmlns","w:profile:picture");
+ }}
+ );
+
+ req.addChild(pic);
+
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true)));
+ }
+
+ private void updateGroups() {
+ groups.clear();
+ {
+ final String reqid = String.valueOf(++iqid);
+ gw1 = iqid;
+ Tree iq = new Tree("list",new HashMap < String,String >() {{ put("type","owning");}} );
+ Tree req = new Tree("iq",
+ new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us"); put("xmlns","w:g"); }} );
+
+ req.addChild(iq);
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true)));
+ }
+ {
+ final String reqid = String.valueOf(++iqid);
+ gw2 = iqid;
+ Tree iq = new Tree("list",
+ new HashMap < String,String >() {{ put("type","participating");}} );
+ Tree req = new Tree("iq",
+ new HashMap < String,String >() {{ put("id",reqid); put("type","get"); put("to","g.us"); put("xmlns","w:g"); }} );
+ req.addChild(iq);
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true)));
+ }
+ gq_stat = 1; // Queried the groups
+ }
+
+ private void manageParticipant(final String group, final String participant, final String command) {
+ Tree part = new Tree("participant",new HashMap < String,String >() {{ put("jid",participant); }} );
+ Tree iq = new Tree(command);
+ iq.addChild(part);
+ final String reqid = String.valueOf(++iqid);
+ Tree req = new Tree("iq",
+ new HashMap < String,String >() {{
+ put("id",reqid); put("type","set"); put("to",group+"@g.us"); put("xmlns","w:g");
+ }}
+ );
+ req.addChild(iq);
+
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true)));
+ }
+
+ private void leaveGroup(final String group) {
+ Tree gr = new Tree("group", new HashMap < String,String >() {{ put("id",group+"@g.us"); }} );
+ Tree iq = new Tree("leave");
+ iq.addChild(gr);
+ final String reqid = String.valueOf(++iqid);
+ Tree req = new Tree("iq",
+ new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us"); put("xmlns","w:g"); }} );
+ req.addChild(iq);
+
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true)));
+ }
+
+ void addGroup(final String subject) {
+ Tree gr = new Tree("group",
+ new HashMap < String,String >() {{ put("action","create"); put("subject",subject);}} );
+ final String reqid = String.valueOf(++iqid);
+ Tree req = new Tree("iq",
+ new HashMap < String,String >() {{ put("id",reqid); put("type","set"); put("to","g.us"); put("xmlns","w:g"); }} );
+ req.addChild(gr);
+
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(req,true)));
+ }
+
+ public byte[] getUserAvatar(String user) {
+ // Look for preview
+ user = MiscUtil.getUser(user);
+ for (int i = 0; i < contacts.size(); i++) {
+ if (contacts.get(i).phone.equals(user)) {
+ return contacts.get(i).ppprev;
+ }
+ }
+ return new byte[0];
+ }
+
+ private void addPreviewPicture(String user, byte [] picture) {
+ user = MiscUtil.getUser(user);
+
+ // Save preview
+ for (int i = 0; i < contacts.size(); i++) {
+ if (contacts.get(i).phone.equals(user)) {
+ contacts.get(i).ppprev = picture;
+ break;
+ }
+ }
+
+ requestVCardUpdate(user);
+ }
+
+ private void requestVCardUpdate(String user) {
+ user = MiscUtil.getUser(user);
+
+ for (int i = 0; i < contacts.size(); i++) {
+ if (contacts.get(i).phone.equals(user)) {
+ VCardUpdate vc = new VCardUpdate();
+ vc.setPhotoHash(MiscUtil.getEncodedSha1Sum(contacts.get(i).ppprev));
+ Presence p = new Presence(Presence.Type.subscribed);
+ p.setTo(phone);
+ p.setFrom(user);
+ p.addExtension(vc);
+ received_packets.add(p);
+ break;
+ }
+ }
+ }
+
+ private void receiveMessage(AbstractMessage msg) {
+ received_packets.add(msg.serializePacket());
+ }
+
+ private void gotTyping(final String user, boolean typing) {
+ Message msg = new Message();
+ msg.setTo(MiscUtil.getUser(phone));
+ msg.setFrom(MiscUtil.getUser(user));
+
+ // Create a MessageEvent Package and add it to the message
+ MessageEvent messageEvent = new MessageEvent();
+ messageEvent.setComposing(true);
+ msg.addExtension(messageEvent);
+ // Send the packet
+ received_packets.add(msg);
+ }
+
+ public Packet getNextPacket() {
+ if (received_packets.size() == 0)
+ return null;
+ Packet r = received_packets.get(0);
+ received_packets.remove(0);
+
+ return r;
+ }
+
+ public void setMyPresence(String pres, String msg) {
+ mypresence = pres;
+ mymessage = msg;
+ if (conn_status == SessionStatus.SessionConnected)
+ notifyMyPresence();
+ }
+
+ private void notifyMyPresence() {
+ // Send the nickname and the current status
+ Tree pres = new Tree("presence", new HashMap < String,String >() {{ put("name",nickname); put("type",mypresence); }} );
+
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(pres,true)));
+ }
+
+ void doPong(final String id, final String from) {
+ Tree t = new Tree("iq",new HashMap < String,String >() {{ put("to",from); put("id",id); put("type","result"); }} );
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(t,true)));
+ }
+
+ public DataBuffer write_tree(Tree tree) {
+ DataBuffer bout = new DataBuffer();
+ int len = 1;
+
+ if (tree.getAttributes().size() != 0) len += tree.getAttributes().size()*2;
+ if (tree.getChildren().size() != 0) len++;
+ if (tree.getData().length != 0 || tree.forcedData()) len++;
+
+ bout.writeListSize(len);
+ if (tree.getTag().equals("start")) bout.putInt(1,1);
+ else bout.putString(tree.getTag());
+ tree.writeAttributes(bout);
+
+ if (tree.getData().length > 0 || tree.forcedData())
+ bout.putRawString(tree.getData());
+ if (tree.getChildren().size() > 0) {
+ bout.writeListSize(tree.getChildren().size());
+
+ for (int i = 0; i < tree.getChildren().size(); i++) {
+ DataBuffer tt = write_tree(tree.getChildren().get(i));
+ bout = bout.addBuf(tt);
+ }
+ }
+ return bout;
+ }
+
+ public DataBuffer serialize_tree(Tree tree, boolean crypt) {
+
+ System.out.println("OUT");
+ System.out.println(tree.toString(0));
+
+ DataBuffer data = write_tree(tree);
+ int flag = 0;
+ if (crypt) {
+ data = data.encodedBuffer(this.out,this.session_key[1],true,frame_seq++);
+ flag = 0x80;
+ }
+
+ System.out.println("OUTDONE");
+
+ DataBuffer ret = new DataBuffer();
+ ret.putInt(flag,1);
+ ret.putInt(data.size(),2);
+ return ret.addBuf(data);
+ }
+
+
+ public void doLogin(String useragent) {
+ DataBuffer first = new DataBuffer();
+
+ {
+ Map < String,String > auth = new HashMap ();
+ auth.put("resource", useragent);
+ auth.put("to", whatsappserver);
+ Tree t = new Tree("start",auth);
+ first.addData( new byte [] {'W','A',1,4} );
+ first = first.addBuf(serialize_tree(t,false));
+ }
+
+ // Send features
+ {
+ Tree p = new Tree("stream:features");
+ first = first.addBuf(serialize_tree(p,false));
+ }
+
+ // Send auth request
+ {
+ Map < String,String > auth = new HashMap ();
+ auth.put("mechanism", "WAUTH-2");
+ auth.put("user", phone);
+ Tree t = new Tree("auth",auth);
+ t.forceDataWrite();
+ first = first.addBuf(serialize_tree(t,false));
+ }
+
+ conn_status = SessionStatus.SessionWaitingChallenge;
+ outbuffer = first;
+ }
+
+ void sendAuthResponse() {
+ Tree t = new Tree("response");
+
+ long epoch = System.currentTimeMillis()/1000;
+ String stime = String.valueOf(epoch);
+ DataBuffer eresponse = new DataBuffer();
+ eresponse.addData(phone.getBytes());
+ eresponse.addData(challenge_data);
+ eresponse.addData(stime.getBytes());
+
+ eresponse = eresponse.encodedBuffer(this.out,this.session_key[1],false, frame_seq++);
+ t.setData(eresponse.getPtr());
+
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(t,false)));
+ }
+
+ public void addContact(String user, boolean user_request) {
+ user = MiscUtil.getUser(user);
+ if (user.contains("-")) return; // Do not add groups as contacts
+
+ boolean found = false;
+ for (int i = 0; i < contacts.size(); i++)
+ if (contacts.get(i).phone.equals(user)) {
+ found = true;
+ return;
+ }
+
+ if (!found) {
+ Contact c = new Contact(user, user_request);
+ contacts.add(c);
+ }
+
+ if (conn_status == SessionStatus.SessionConnected) {
+ subscribePresence(user);
+ queryPreview(user);
+ getLastSeen(user);
+ }
+ }
+
+ public void subscribePresence(String user) {
+ final String username = MiscUtil.getUser(user)+"@"+whatsappserver;
+ Tree request = new Tree("presence",
+ new HashMap < String,String >() {{ put("type","subscribe"); put("to",username); }} );
+
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(request,true)));
+
+ // Meanwhile add the user presence...
+ Presence.Mode mode = Presence.Mode.away;
+ Presence presp = new Presence(Presence.Type.available);
+ presp.setMode(mode);
+ presp.setFrom(MiscUtil.getUser(user));
+ presp.setTo(MiscUtil.getUser(phone));
+ received_packets.add(presp);
+ }
+
+ private void getLastSeen(final String user) {
+ final String id = String.valueOf(++iqid);
+ final String fuser = MiscUtil.getUser(user)+"@"+whatsappserver;
+ Tree iq = new Tree("iq",
+ new HashMap < String,String >() {{
+ put("id",id); put("type","get"); put("to",fuser); put("xmlns","jabber:iq:last");
+ }}
+ );
+ iq.addChild(new Tree("query"));
+
+ outbuffer = outbuffer.addBuf(new DataBuffer(serialize_tree(iq,true)));
+ }
+
+ public byte [] getWriteData() {
+ byte [] r = outbuffer.getPtr();
+ System.out.println("Sending some bytes ... " + String.valueOf(r.length));
+ outbuffer = new DataBuffer();
+ return r;
+ }
+
+ // Helper for Message class
+ public byte [] serializeMessage(final String to, String message, int id) {
+ try {
+ Tree tbody = new Tree("body");
+ tbody.setData(message.getBytes("UTF-8"));
+
+ long epoch = System.currentTimeMillis()/1000;
+ String stime = String.valueOf(epoch);
+ Map < String,String > attrs = new HashMap ();
+ String full_to = to + "@" + (to.contains("-") ? whatsappservergroup : whatsappserver);
+ attrs.put("to",full_to);
+ attrs.put("type","chat");
+ attrs.put("id",stime+"-"+String.valueOf(id));
+ attrs.put("t",stime);
+
+ Tree mes = new Tree("message",attrs);
+ mes.addChild(tbody);
+
+ return serialize_tree(mes,true).getPtr();
+ }catch (Exception e) {
+ return new byte[0];
+ }
+ }
+
+ public abstract class AbstractMessage {
+ protected String from, id, author;
+ protected long time;
+
+ public AbstractMessage(String from, long time, String id, String author) {
+ this.from = from;
+ this.id = id;
+ this.author = author;
+ this.time = time;
+ }
+
+ public abstract Packet serializePacket();
+ }
+
+ public class ChatMessage extends AbstractMessage {
+ private String message;
+ public ChatMessage(String from, long time, String id, String message, String author) {
+ super(from, time, id, author);
+ this.message = message;
+ }
+
+ public Packet serializePacket() {
+ Message message = new Message();
+ message.setTo(MiscUtil.getUser(phone));
+ message.setFrom(MiscUtil.getUserAndResource(this.from));
+ message.setType(Message.Type.chat);
+ message.setBody(this.message);
+
+ // XXX: Criteria for adding Delay info is
+ // if the timestamp and the current time differ in more than 10 seconds
+ DelayInformation d = new DelayInformation(new Date(time*1000));
+ long epoch = System.currentTimeMillis()/1000;
+ if (Math.abs(time - epoch) > 10)
+ message.addExtension(d);
+
+ return message;
+ }
+ }
+ public class ImageMessage extends AbstractMessage {
+ private String url;
+ private byte [] preview;
+ public ImageMessage(String from, long time, String id, String url, byte [] prev, String author) {
+ super(from, time, id, author);
+ this.url = url;
+ this.preview = prev;
+ }
+
+ public Packet serializePacket() {
+ Message message = new Message();
+ message.setTo(MiscUtil.getUser(phone));
+ message.setFrom(MiscUtil.getUserAndResource(this.from));
+ message.setType(Message.Type.chat);
+ message.setBody(url);
+
+ if (author != null && author.length() != 0)
+ message.addExtension(new Nick(author));
+
+ // XXX: Criteria for adding Delay info is
+ // if the timestamp and the current time differ in more than 10 seconds
+ DelayInformation d = new DelayInformation(new Date(time*1000));
+ long epoch = System.currentTimeMillis()/1000;
+ if (Math.abs(time - epoch) > 10)
+ message.addExtension(d);
+
+ return message;
+ }
+ }
+
+
+ public class Contact {
+ String phone, name;
+ String presence, typing;
+ String status;
+ long last_seen, last_status;
+ boolean mycontact;
+ byte[] ppprev, pppicture;
+ boolean subscribed;
+
+ Contact(String phone, boolean myc) {
+ this.phone = phone;
+ this.mycontact = myc;
+ this.last_seen = 0;
+ this.subscribed = false;
+ this.typing = "paused";
+ this.status = "";
+ }
+ };
+
+ public class Group {
+ String id, subject, owner;
+ Vector participants;
+
+ Group(String id, String subject, String owner) {
+ this.id = id;
+ this.subject = subject;
+ this.owner = owner;
+ participants = new Vector ();
+ }
+ };
+
+}
+
+
diff --git a/src/org/jivesoftware/smack/Connection.java b/src/org/jivesoftware/smack/Connection.java
index a96567f2d1..f3fd899388 100644
--- a/src/org/jivesoftware/smack/Connection.java
+++ b/src/org/jivesoftware/smack/Connection.java
@@ -226,6 +226,10 @@ protected ConnectionConfiguration getConfiguration() {
public String getServiceName() {
return config.getServiceName();
}
+
+ public boolean isAlive() {
+ return true;
+ }
/**
* Returns the host name of the server where the XMPP server is running. This would be the
@@ -329,6 +333,10 @@ protected boolean isReconnectionAllowed() {
* @throws XMPPException if an error occurs while trying to establish the connection.
*/
public abstract void connect() throws XMPPException;
+
+ public void connect(final String user, final String pass, final String res) throws XMPPException {
+ connect();
+ }
/**
* Logs in to the server using the strongest authentication mode supported by
diff --git a/src/org/jivesoftware/smack/XMPPConnection.java b/src/org/jivesoftware/smack/XMPPConnection.java
index 809955207b..aadd182793 100644
--- a/src/org/jivesoftware/smack/XMPPConnection.java
+++ b/src/org/jivesoftware/smack/XMPPConnection.java
@@ -1095,6 +1095,7 @@ public void setRosterStorage(RosterStorage storage)
*
* @return false if timeout occur.
*/
+ @Override
public boolean isAlive() {
PacketWriter packetWriter = this.packetWriter;
return packetWriter == null || packetWriter.isAlive();