package info.guardianproject.otr.app.im.plugin.xmpp;
import info.guardianproject.otr.TorProxyInfo;
import info.guardianproject.otr.app.im.R;
import info.guardianproject.otr.app.im.app.DatabaseUtils;
import info.guardianproject.otr.app.im.app.ImApp;
import info.guardianproject.otr.app.im.engine.Address;
import info.guardianproject.otr.app.im.engine.ChatGroup;
import info.guardianproject.otr.app.im.engine.ChatGroupManager;
import info.guardianproject.otr.app.im.engine.ChatSession;
import info.guardianproject.otr.app.im.engine.ChatSessionManager;
import info.guardianproject.otr.app.im.engine.Contact;
import info.guardianproject.otr.app.im.engine.ContactList;
import info.guardianproject.otr.app.im.engine.ContactListListener;
import info.guardianproject.otr.app.im.engine.ContactListManager;
import info.guardianproject.otr.app.im.engine.ImConnection;
import info.guardianproject.otr.app.im.engine.ImErrorInfo;
import info.guardianproject.otr.app.im.engine.ImException;
import info.guardianproject.otr.app.im.engine.Invitation;
import info.guardianproject.otr.app.im.engine.Message;
import info.guardianproject.otr.app.im.engine.Presence;
import info.guardianproject.otr.app.im.plugin.xmpp.auth.GTalkOAuth2;
import info.guardianproject.otr.app.im.provider.Imps;
import info.guardianproject.otr.app.im.provider.ImpsErrorInfo;
import info.guardianproject.util.DNSUtil;
import info.guardianproject.util.Debug;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import org.apache.harmony.javax.security.auth.callback.Callback;
import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.RosterGroup;
import org.jivesoftware.smack.RosterListener;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.DefaultPacketExtension;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence.Mode;
import org.jivesoftware.smack.packet.Presence.Type;
import org.jivesoftware.smack.provider.PrivacyProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.proxy.ProxyInfo;
import org.jivesoftware.smack.proxy.ProxyInfo.ProxyType;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.FormField;
import org.jivesoftware.smackx.GroupChatInvitation;
import org.jivesoftware.smackx.PrivateDataManager;
import org.jivesoftware.smackx.ServiceDiscoveryManager;
import org.jivesoftware.smackx.bytestreams.socks5.provider.BytestreamsProvider;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.muc.RoomInfo;
import org.jivesoftware.smackx.packet.ChatStateExtension;
import org.jivesoftware.smackx.packet.LastActivity;
import org.jivesoftware.smackx.packet.OfflineMessageInfo;
import org.jivesoftware.smackx.packet.OfflineMessageRequest;
import org.jivesoftware.smackx.packet.SharedGroupsInfo;
import org.jivesoftware.smackx.packet.VCard;
import org.jivesoftware.smackx.provider.AdHocCommandDataProvider;
import org.jivesoftware.smackx.provider.DataFormProvider;
import org.jivesoftware.smackx.provider.DelayInformationProvider;
import org.jivesoftware.smackx.provider.DiscoverInfoProvider;
import org.jivesoftware.smackx.provider.DiscoverItemsProvider;
import org.jivesoftware.smackx.provider.MUCAdminProvider;
import org.jivesoftware.smackx.provider.MUCOwnerProvider;
import org.jivesoftware.smackx.provider.MUCUserProvider;
import org.jivesoftware.smackx.provider.MessageEventProvider;
import org.jivesoftware.smackx.provider.MultipleAddressesProvider;
import org.jivesoftware.smackx.provider.RosterExchangeProvider;
import org.jivesoftware.smackx.provider.StreamInitiationProvider;
import org.jivesoftware.smackx.provider.VCardProvider;
import org.jivesoftware.smackx.provider.XHTMLExtensionProvider;
import org.jivesoftware.smackx.search.UserSearch;
import org.thoughtcrime.ssl.pinning.PinningTrustManager;
import org.thoughtcrime.ssl.pinning.SystemKeyStore;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import de.duenndns.ssl.MemorizingTrustManager;
public class XmppConnection extends ImConnection implements CallbackHandler {
private static final String DISCO_FEATURE = "http://jabber.org/protocol/disco#info";
final static String TAG = "GB.XmppConnection";
private final static boolean PING_ENABLED = true;
private XmppContactListManager mContactListManager;
private Contact mUser;
// watch out, this is a different XMPPConnection class than XmppConnection! ;)
// Synchronized by executor thread
private MyXMPPConnection mConnection;
private XmppStreamHandler mStreamHandler;
private XmppChatSessionManager mSessionManager;
private ConnectionConfiguration mConfig;
// True if we are in the process of reconnecting. Reconnection is retried once per heartbeat.
// Synchronized by executor thread.
private boolean mNeedReconnect;
private boolean mRetryLogin;
private ThreadPoolExecutor mExecutor;
private ProxyInfo mProxyInfo = null;
private long mAccountId = -1;
private long mProviderId = -1;
private String mPasswordTemp;
private boolean mIsGoogleAuth = false;
/*
private final static String TRUSTSTORE_TYPE = "BKS";
private final static String TRUSTSTORE_PATH = "debiancacerts.bks";
private final static String TRUSTSTORE_PASS = "changeit";
private final static String KEYMANAGER_TYPE = "X509";
*/
private final static String SSLCONTEXT_TYPE = "TLS";
private X509TrustManager mTrustManager;
//private StrongTrustManager mStrongTrustManager;
private SSLContext sslContext;
private KeyStore ks = null;
private KeyManager[] kms = null;
private Context aContext;
private final static String IS_GOOGLE = "google";
private final static int SOTIMEOUT = 15000;
private PacketCollector mPingCollector;
private String mUsername;
private String mPassword;
private String mResource;
private int mPriority;
private int mGlobalId;
private static int mGlobalCount;
private final Random rndForTorCircuits = new Random();
// Maintains a sequence counting up to the user configured heartbeat interval
private int heartbeatSequence = 0;
LinkedBlockingQueue<String> qAvatar = new LinkedBlockingQueue <String>();
public XmppConnection(Context context) throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
super(context);
synchronized (XmppConnection.class) {
mGlobalId = mGlobalCount++;
}
aContext = context;
Debug.onConnectionStart();
//setup SSL managers
SmackConfiguration.setPacketReplyTimeout(SOTIMEOUT);
// Create a single threaded executor. This will serialize actions on the underlying connection.
createExecutor();
addProviderManagerExtensions();
XmppStreamHandler.addExtensionProviders();
DeliveryReceipts.addExtensionProviders();
ServiceDiscoveryManager.setIdentityName("Gibberbot");
ServiceDiscoveryManager.setIdentityType("phone");
mUser = makeUser();
}
Contact makeUser() {
ContentResolver contentResolver = mContext.getContentResolver();
Imps.ProviderSettings.QueryMap providerSettings = new Imps.ProviderSettings.QueryMap(
contentResolver, mProviderId, false, null);
String userName = Imps.Account.getUserName(contentResolver, mAccountId);
String domain = providerSettings.getDomain();
String xmppName = userName + '@' + domain;// + '/' + providerSettings.getXmppResource();
providerSettings.close();
return new Contact(new XmppAddress(xmppName), userName);
}
private void createExecutor() {
mExecutor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
private boolean execute(Runnable runnable) {
try {
mExecutor.execute(runnable);
} catch (RejectedExecutionException ex) {
return false;
}
return true;
}
// Execute a runnable only if we are idle
private boolean executeIfIdle(Runnable runnable) {
if (mExecutor.getActiveCount() + mExecutor.getQueue().size() == 0) {
return execute(runnable);
}
return false;
}
// This runs in executor thread, and since there is only one such thread, we will definitely
// succeed in shutting down the executor if we get here.
public void join() {
final ExecutorService executor = mExecutor;
mExecutor = null;
// This will send us an interrupt, which we will ignore. We will terminate
// anyway after the caller is done. This also drains the executor queue.
if (executor != null)
executor.shutdownNow();
}
// For testing
boolean joinGracefully() throws InterruptedException {
final ExecutorService executor = mExecutor;
mExecutor = null;
// This will send us an interrupt, which we will ignore. We will terminate
// anyway after the caller is done. This also drains the executor queue.
if (executor != null) {
executor.shutdown();
return executor.awaitTermination(1, TimeUnit.SECONDS);
}
return false;
}
public void sendPacket(final org.jivesoftware.smack.packet.Packet packet) {
execute(new Runnable() {
@Override
public void run() {
if (mConnection == null) {
Log.w(TAG, "postponed packet to " + packet.getTo()
+ " because we are not connected");
postpone(packet);
return;
}
try {
mConnection.sendPacket(packet);
} catch (IllegalStateException ex) {
postpone(packet);
Log.w(TAG, "postponed packet to " + packet.getTo()
+ " because socket is disconnected");
}
}
});
}
void postpone(final org.jivesoftware.smack.packet.Packet packet) {
if (packet instanceof org.jivesoftware.smack.packet.Message) {
ChatSession session = findOrCreateSession(packet.getTo());
session.onMessagePostponed(packet.getPacketID());
}
}
/*
private void loadVCardsAsync ()
{
// Using an AsyncTask to load the slow images in a background thread
new AsyncTask<String, Void, String>() {
@Override
protected String doInBackground(String... params) {
loadVCards();
return "";
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
}
}.execute("");
}
private void loadVCards ()
{
String jid = null;
ContentResolver resolver = mContext.getContentResolver();
try
{
while ((jid = qAvatar.poll(1000, TimeUnit.MILLISECONDS)) != null)
{
loadVCard (resolver, jid);
}
}
catch (Exception e) {}
};*/
private boolean loadVCard (ContentResolver resolver, String jid, String hash)
{
try {
if ((!DatabaseUtils.doesAvatarHashExist(resolver, Imps.Avatars.CONTENT_URI, jid, hash)))
{
debug(ImApp.LOG_TAG, "loading vcard for: " + jid);
VCard vCard = new VCard();
// FIXME synchronize this to executor thread
vCard.load(mConnection, jid);
// If VCard is loaded, then save the avatar to the personal folder.
String avatarHash = vCard.getAvatarHash();
if (avatarHash != null)
{
byte[] avatarBytes = vCard.getAvatar();
if (avatarBytes != null)
{
debug(ImApp.LOG_TAG, "found avatar image in vcard for: " + jid);
debug(ImApp.LOG_TAG, "start avatar length: " + avatarBytes.length);
int width = ImApp.DEFAULT_AVATAR_WIDTH;
int height = ImApp.DEFAULT_AVATAR_HEIGHT;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(avatarBytes, 0, avatarBytes.length,options);
options.inSampleSize = DatabaseUtils.calculateInSampleSize(options, width, height);
options.inJustDecodeBounds = false;
Bitmap b = BitmapFactory.decodeByteArray(avatarBytes, 0, avatarBytes.length,options);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
b.compress(Bitmap.CompressFormat.JPEG, 70, stream);
byte[] avatarBytesCompressed = stream.toByteArray();
debug(ImApp.LOG_TAG, "compressed avatar length: " + avatarBytesCompressed.length);
DatabaseUtils.insertAvatarBlob(resolver, Imps.Avatars.CONTENT_URI, mProviderId, mAccountId, avatarBytesCompressed, avatarHash, jid);
// int providerId, int accountId, byte[] data, String hash,String contact
return true;
}
}
}
} catch (XMPPException e) {
Log.d(ImApp.LOG_TAG,"err loading vcard");
if (e.getStreamError() != null)
{
String streamErr = e.getStreamError().getCode();
if (streamErr != null && (streamErr.contains("404") || streamErr.contains("503")))
{
return false;
}
}
}
return false;
}
@Override
protected void doUpdateUserPresenceAsync(Presence presence) {
org.jivesoftware.smack.packet.Presence packet = makePresencePacket(presence);
sendPacket(packet);
mUserPresence = presence;
notifyUserPresenceUpdated();
}
private org.jivesoftware.smack.packet.Presence makePresencePacket(Presence presence) {
String statusText = presence.getStatusText();
Type type = Type.available;
Mode mode = Mode.available;
int priority = mPriority;
final int status = presence.getStatus();
if (status == Presence.AWAY) {
priority = 10;
mode = Mode.away;
} else if (status == Presence.IDLE) {
priority = 15;
mode = Mode.away;
} else if (status == Presence.DO_NOT_DISTURB) {
priority = 5;
mode = Mode.dnd;
} else if (status == Presence.OFFLINE) {
priority = 0;
type = Type.unavailable;
statusText = "Offline";
}
// The user set priority is the maximum allowed
if (priority > mPriority)
priority = mPriority;
org.jivesoftware.smack.packet.Presence packet = new org.jivesoftware.smack.packet.Presence(
type, statusText, priority, mode);
return packet;
}
@Override
public int getCapability() {
return ImConnection.CAPABILITY_SESSION_REESTABLISHMENT | ImConnection.CAPABILITY_GROUP_CHAT;
}
private XmppChatGroupManager mChatGroupManager = null;
@Override
public synchronized ChatGroupManager getChatGroupManager() {
if (mChatGroupManager == null)
mChatGroupManager = new XmppChatGroupManager();
return mChatGroupManager;
}
public class XmppChatGroupManager extends ChatGroupManager
{
private Hashtable<String,MultiUserChat> mMUCs = new Hashtable<String,MultiUserChat>();
public MultiUserChat getMultiUserChat (String chatRoomJid)
{
return mMUCs.get(chatRoomJid);
}
@Override
public boolean createChatGroupAsync(String chatRoomJid) throws Exception {
RoomInfo roomInfo = null;
Address address = new XmppAddress (chatRoomJid);
try
{
//first check if the room already exists
roomInfo = MultiUserChat.getRoomInfo(mConnection, chatRoomJid);
}
catch (Exception e)
{
//who knows?
}
if (roomInfo == null)
{
//if the room does not exist, then create one
//should be room@server
String[] parts = chatRoomJid.split("@");
String room = parts[0];
String server = parts[1];
String nickname = mUser.getName().split("@")[0];
try {
// Create a MultiUserChat using a Connection for a room
MultiUserChat muc = new MultiUserChat(mConnection, chatRoomJid);
try
{
// Create the room
muc.create(nickname);
}
catch (XMPPException iae)
{
if (iae.getMessage().contains("Creation failed"))
{
//some server's don't return the proper 201 create code, so we can just assume the room was created!
}
else
{
throw iae;
}
}
try
{
Form form = muc.getConfigurationForm();
Form submitForm = form.createAnswerForm();
for (Iterator fields = form.getFields();fields.hasNext();){
FormField field = (FormField) fields.next();
if(!FormField.TYPE_HIDDEN.equals(field.getType()) && field.getVariable()!= null){
submitForm.setDefaultAnswer(field.getVariable());
}
}
submitForm.setAnswer("muc#roomconfig_publicroom", true);
muc.sendConfigurationForm(submitForm);
}
catch (XMPPException xe)
{
if (Debug.DEBUG_ENABLED)
Log.w(ImApp.LOG_TAG,"(ignoring) got an error configuring MUC room: " + xe.getLocalizedMessage());
}
muc.join(nickname);
ChatGroup chatGroup = new ChatGroup(address,room,this);
mGroups.put(address.getAddress(), chatGroup);
mMUCs.put(chatRoomJid, muc);
return true;
} catch (XMPPException e) {
Log.e(ImApp.LOG_TAG,"error creating MUC",e);
return false;
}
}
else
{
//otherwise, join the room!
joinChatGroupAsync(address);
return true;
}
}
@Override
public void deleteChatGroupAsync(ChatGroup group) {
String chatRoomJid = group.getAddress().getAddress();
if (mMUCs.containsKey(chatRoomJid))
{
MultiUserChat muc = mMUCs.get(chatRoomJid);
try {
muc.destroy("", null);
mMUCs.remove(chatRoomJid);
} catch (XMPPException e) {
Log.e(ImApp.LOG_TAG,"error destroying MUC",e);
}
}
}
@Override
protected void addGroupMemberAsync(ChatGroup group, Contact contact) {
// TODO Auto-generated method stub
}
@Override
protected void removeGroupMemberAsync(ChatGroup group, Contact contact) {
// TODO Auto-generated method stub
}
@Override
public void joinChatGroupAsync(Address address) {
String chatRoomJid = address.getAddress();
String[] parts = chatRoomJid.split("@");
String room = parts[0];
String server = parts[1];
String nickname = mUser.getName().split("@")[0];
try {
// Create a MultiUserChat using a Connection for a room
MultiUserChat muc = new MultiUserChat(mConnection, chatRoomJid);
// Create the room
muc.join(nickname);
ChatGroup chatGroup = new ChatGroup(address,room,this);
mGroups.put(address.getAddress(), chatGroup);
mMUCs.put(chatRoomJid, muc);
} catch (XMPPException e) {
Log.e(ImApp.LOG_TAG,"error joining MUC",e);
}
}
@Override
public void leaveChatGroupAsync(ChatGroup group) {
String chatRoomJid = group.getAddress().getAddress();
if (mMUCs.containsKey(chatRoomJid))
{
MultiUserChat muc = mMUCs.get(chatRoomJid);
muc.leave();
mMUCs.remove(chatRoomJid);
}
}
@Override
public void inviteUserAsync(ChatGroup group, Contact invitee) {
String chatRoomJid = group.getAddress().getAddress();
if (mMUCs.containsKey(chatRoomJid))
{
MultiUserChat muc = mMUCs.get(chatRoomJid);
String reason = ""; //no reason for now
muc.invite(invitee.getAddress().getAddress(),reason);
}
}
@Override
public void acceptInvitationAsync(Invitation invitation) {
Address addressGroup = invitation.getGroupAddress();
joinChatGroupAsync (addressGroup);
}
@Override
public void rejectInvitationAsync(Invitation invitation) {
Address addressGroup = invitation.getGroupAddress();
String reason = ""; // no reason for now
MultiUserChat.decline(mConnection, addressGroup.getAddress(),invitation.getSender().getAddress(),reason);
}
};
@Override
public synchronized ChatSessionManager getChatSessionManager() {
if (mSessionManager == null)
mSessionManager = new XmppChatSessionManager();
return mSessionManager;
}
@Override
public synchronized XmppContactListManager getContactListManager() {
if (mContactListManager == null)
mContactListManager = new XmppContactListManager();
return mContactListManager;
}
@Override
public Contact getLoginUser() {
return mUser;
}
@Override
public Map<String, String> getSessionContext() {
// Empty state for now (but must have at least one key)
return Collections.singletonMap("state", "empty");
}
@Override
public int[] getSupportedPresenceStatus() {
return new int[] { Presence.AVAILABLE, Presence.AWAY, Presence.IDLE, Presence.OFFLINE,
Presence.DO_NOT_DISTURB, };
}
@Override
public void loginAsync(long accountId, String passwordTemp, long providerId, boolean retry) {
mAccountId = accountId;
mPasswordTemp = passwordTemp;
mProviderId = providerId;
mRetryLogin = retry;
mUser = makeUser();
execute(new Runnable() {
@Override
public void run() {
do_login();
}
});
}
// Runs in executor thread
private void do_login() {
if (mConnection != null) {
setState(getState(), new ImErrorInfo(ImErrorInfo.CANT_CONNECT_TO_SERVER,
"still trying..."));
return;
}
ContentResolver contentResolver = mContext.getContentResolver();
Imps.ProviderSettings.QueryMap providerSettings = new Imps.ProviderSettings.QueryMap(
contentResolver, mProviderId, false, null);
// providerSettings is closed in initConnection();
String userName = Imps.Account.getUserName(contentResolver, mAccountId);
String password = Imps.Account.getPassword(contentResolver, mAccountId);
String defaultStatus = null;
mNeedReconnect = true;
setState(LOGGING_IN, null);
mUserPresence = new Presence(Presence.AVAILABLE, defaultStatus, Presence.CLIENT_TYPE_MOBILE);
try {
if (userName.length() == 0)
throw new XMPPException("empty username not allowed");
initConnectionAndLogin(providerSettings, userName, password);
// TODO should we really be using the same name for both address and name?
setState(LOGGED_IN, null);
debug(TAG, "logged in");
} catch (Exception e) {
debug(TAG, "login failed: " + e.getLocalizedMessage());
mConnection = null;
ImErrorInfo info = new ImErrorInfo(ImErrorInfo.CANT_CONNECT_TO_SERVER, e.getMessage());
if (e == null || e.getMessage() == null) {
debug(TAG, "NPE: " + e.getMessage());
Log.e(TAG,"login error",e);
info = new ImErrorInfo(ImErrorInfo.INVALID_USERNAME, "unknown error");
disconnected(info);
mRetryLogin = false;
} else if (e.getMessage().contains("not-authorized")
|| e.getMessage().contains("authentication failed")) {
if (mIsGoogleAuth && password.contains(GTalkOAuth2.NAME))
{
debug (TAG, "google failed; may need to refresh");
password = refreshGoogleToken (userName, password,providerSettings.getDomain());
mRetryLogin = true;
setState(LOGGING_IN, info);
}
else
{
debug(TAG, "not authorized - will not retry");
info = new ImErrorInfo(ImErrorInfo.INVALID_USERNAME, "invalid user/password");
disconnected(info);
mRetryLogin = false;
}
} else if (mRetryLogin) {
debug(TAG, "will retry");
setState(LOGGING_IN, info);
} else {
debug(TAG, "will not retry");
mConnection = null;
disconnected(info);
}
return;
} finally {
mNeedReconnect = false;
providerSettings.close();
}
}
private String refreshGoogleToken (String userName, String oldPassword, String domain)
{
//invalidate our old one, that is locally cached
AccountManager.get(mContext.getApplicationContext()).invalidateAuthToken("com.google", oldPassword.split(":")[1]);
//request a new one
String password = GTalkOAuth2.getGoogleAuthToken(userName + '@' + domain, mContext.getApplicationContext());
password = GTalkOAuth2.NAME + ':' + password;
//now store the new one, for future use until it expires
final long accountId = ImApp.insertOrUpdateAccount(mContext.getContentResolver(), mProviderId, userName,
password );
return password;
}
// TODO shouldn't setProxy be handled in Imps/settings?
public void setProxy(String type, String host, int port) {
if (type == null) {
mProxyInfo = ProxyInfo.forNoProxy();
} else {
ProxyInfo.ProxyType pType = ProxyType.valueOf(type);
String username = null;
String password = null;
if (type.equals(TorProxyInfo.PROXY_TYPE) //socks5
&& host.equals(TorProxyInfo.PROXY_HOST) //127.0.0.1
&& port == TorProxyInfo.PROXY_PORT) //9050
{
//if the proxy is for Orbot/Tor then generate random usr/pwd to isolate Tor streams
username = rndForTorCircuits.nextInt(100000)+"";
password = rndForTorCircuits.nextInt(100000)+"";
}
mProxyInfo = new ProxyInfo(pType, host, port, username, password);
}
}
public void initConnection(MyXMPPConnection connection, Contact user, int state) {
mConnection = connection;
mUser = user;
setState(state, null);
}
private void initConnectionAndLogin (Imps.ProviderSettings.QueryMap providerSettings,String userName, String password) throws Exception
{
Debug.onConnectionStart(); //only activates if Debug TRUE is set, so you can leave this in!
if (mPasswordTemp != null)
password = mPasswordTemp;
mIsGoogleAuth = password.startsWith(GTalkOAuth2.NAME);
String domain = providerSettings.getDomain();
String server = providerSettings.getServer();
if (mIsGoogleAuth)
{
password = refreshGoogleToken(userName, password, domain);
password = password.split(":")[1];
// mUsername = userName + '@' + domain;
}
initConnection(providerSettings, userName);
mPassword = password;
mResource = providerSettings.getXmppResource();
//disable compression based on statement by Ge0rg
mConfig.setCompressionEnabled(false);
mConnection.login(mUsername, mPassword, mResource);
mStreamHandler.notifyInitialLogin();
initServiceDiscovery();
sendPresencePacket();
Roster roster = mConnection.getRoster();
roster.setSubscriptionMode(Roster.SubscriptionMode.manual);
getContactListManager().listenToRoster(roster);
}
// Runs in executor thread
private void initConnection(Imps.ProviderSettings.QueryMap providerSettings, String userName) throws Exception {
boolean allowPlainAuth = providerSettings.getAllowPlainAuth();
boolean requireTls = providerSettings.getRequireTls();
boolean doDnsSrv = providerSettings.getDoDnsSrv();
boolean tlsCertVerify = providerSettings.getTlsCertVerify();
boolean useSASL = true;//!allowPlainAuth;
String domain = providerSettings.getDomain();
String requestedServer = providerSettings.getServer();
if ("".equals(requestedServer))
requestedServer = null;
mPriority = providerSettings.getXmppResourcePrio();
int serverPort = providerSettings.getPort();
String server = requestedServer;
debug(TAG, "TLS required? " + requireTls);
debug(TAG, "cert verification? " + tlsCertVerify);
if (providerSettings.getUseTor()) {
Log.i("temp", "user " + userName + " use tor");
setProxy(TorProxyInfo.PROXY_TYPE, TorProxyInfo.PROXY_HOST,
TorProxyInfo.PROXY_PORT);
}
else
{
setProxy(null, null, -1);
}
if (mProxyInfo == null)
mProxyInfo = ProxyInfo.forNoProxy();
// If user did not specify a server, and SRV requested then lookup SRV
if (doDnsSrv && requestedServer == null) {
//java.lang.System.setProperty("java.net.preferIPv4Stack", "true");
//java.lang.System.setProperty("java.net.preferIPv6Addresses", "false");
debug(TAG, "(DNS SRV) resolving: " + domain);
DNSUtil.HostAddress srvHost = DNSUtil.resolveXMPPDomain(domain);
server = srvHost.getHost();
if (serverPort <= 0) {
// If user did not override port, use port from SRV record
serverPort = srvHost.getPort();
}
debug(TAG, "(DNS SRV) resolved: " + domain + "=" + server + ":" + serverPort);
}
if (server != null && server.contains("google.com"))
{
mUsername = userName + '@' + domain;
}
else if (domain.contains("gmail.com"))
{
mUsername = userName + '@' + domain;
}
else if (mIsGoogleAuth)
{
mUsername = userName + '@' + domain;
}
else
{
mUsername = userName;
}
if (serverPort == 0)
serverPort = 5222;
// No server requested and SRV lookup wasn't requested or returned nothing - use domain
if (server == null) {
debug(TAG, "(use domain) ConnectionConfiguration(" + domain + ", " + serverPort + ", "
+ domain + ", mProxyInfo);");
if (mProxyInfo == null)
mConfig = new ConnectionConfiguration(domain, serverPort);
else
mConfig = new ConnectionConfiguration(domain, serverPort, mProxyInfo);
server = domain;
} else {
debug(TAG, "(use server) ConnectionConfiguration(" + server + ", " + serverPort + ", "
+ domain + ", mProxyInfo);");
if (mProxyInfo == null)
mConfig = new ConnectionConfiguration(server, serverPort, domain);
else
mConfig = new ConnectionConfiguration(server, serverPort, domain, mProxyInfo);
}
// mConfig.setDebuggerEnabled(Debug.DEBUG_ENABLED);
mConfig.setSASLAuthenticationEnabled(useSASL);
// Android has no support for Kerberos or GSSAPI, so disable completely
SASLAuthentication.unregisterSASLMechanism("KERBEROS_V4");
SASLAuthentication.unregisterSASLMechanism("GSSAPI");
//add gtalk auth in
SASLAuthentication.supportSASLMechanism("PLAIN", 1);
SASLAuthentication.supportSASLMechanism("DIGEST-MD5", 2);
if (requireTls) {
mConfig.setSecurityMode(SecurityMode.required);
mConfig.setVerifyChainEnabled(true);
mConfig.setVerifyRootCAEnabled(true);
mConfig.setExpiredCertificatesCheckEnabled(true);
mConfig.setNotMatchingDomainCheckEnabled(true);
mConfig.setSelfSignedCertificateEnabled(false);
// Per XMPP specs, cert must match domain, not SRV lookup result. Otherwise, DNS spoofing
// can enable MITM.
if (sslContext == null)
{
sslContext = SSLContext.getInstance(SSLCONTEXT_TYPE);
mTrustManager = getTrustManager ();
SecureRandom mSecureRandom = new java.security.SecureRandom();
sslContext.init(null, new javax.net.ssl.TrustManager[] { mTrustManager },
mSecureRandom);
sslContext.getDefaultSSLParameters().setCipherSuites(XMPPCertPins.SSL_IDEAL_CIPHER_SUITES);
}
mConfig.setCustomSSLContext(sslContext);
int currentapiVersion = android.os.Build.VERSION.SDK_INT;
if (currentapiVersion >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH){
mConfig.setEnabledCipherSuites(XMPPCertPins.SSL_IDEAL_CIPHER_SUITES);
}
mConfig.setCallbackHandler(this);
} else {
// if it finds a cert, still use it, but don't check anything since
// TLS errors are not expected by the user
mConfig.setSecurityMode(SecurityMode.enabled);
mConfig.setSocketFactory(new DummySSLSocketFactory(getTrustManager()));
if (!allowPlainAuth)
SASLAuthentication.unsupportSASLMechanism("PLAIN");
mConfig.setVerifyChainEnabled(false);
mConfig.setVerifyRootCAEnabled(false);
mConfig.setExpiredCertificatesCheckEnabled(false);
mConfig.setNotMatchingDomainCheckEnabled(false);
mConfig.setSelfSignedCertificateEnabled(true);
}
if (mIsGoogleAuth)
{
mConfig.setSASLAuthenticationEnabled(true);
SASLAuthentication.registerSASLMechanism( GTalkOAuth2.NAME, GTalkOAuth2.class );
SASLAuthentication.supportSASLMechanism( GTalkOAuth2.NAME, 0);
}
else
{
SASLAuthentication.unregisterSASLMechanism( GTalkOAuth2.NAME);
SASLAuthentication.unsupportSASLMechanism( GTalkOAuth2.NAME);
}
// Don't use smack reconnection - not reliable
mConfig.setReconnectionAllowed(false);
mConfig.setSendPresence(true);
mConfig.setRosterLoadedAtLogin(true);
mConnection = new MyXMPPConnection(mConfig);
//debug(TAG,"is secure connection? " + mConnection.isSecureConnection());
//debug(TAG,"is using TLS? " + mConnection.isUsingTLS());
mConnection.addPacketListener(new PacketListener() {
@Override
public void processPacket(Packet packet) {
debug(TAG, "receive message: " + packet.getFrom() + " to " + packet.getTo());
org.jivesoftware.smack.packet.Message smackMessage = (org.jivesoftware.smack.packet.Message) packet;
String address = smackMessage.getFrom();
String body = smackMessage.getBody();
DeliveryReceipts.DeliveryReceipt dr = (DeliveryReceipts.DeliveryReceipt) smackMessage
.getExtension("received", DeliveryReceipts.NAMESPACE);
if (dr != null) {
debug(TAG, "got delivery receipt for " + dr.getId());
ChatSession session = findOrCreateSession(address);
session.onMessageReceipt(dr.getId());
}
if (body != null)
{
ChatSession session = findOrCreateSession(address);
Message rec = new Message(body);
rec.setTo(mUser.getAddress());
rec.setFrom(new XmppAddress(smackMessage.getFrom()));
rec.setDateTime(new Date());
rec.setType(Imps.MessageType.INCOMING);
// Detect if this was said by us, and mark message as outgoing
if (smackMessage.getType() == org.jivesoftware.smack.packet.Message.Type.groupchat &&
rec.getFrom().getResource().equals(rec.getTo().getUser())) {
rec.setType(Imps.MessageType.OUTGOING);
}
boolean good = session.onReceiveMessage(rec);
if (smackMessage.getExtension("request", DeliveryReceipts.NAMESPACE) != null) {
if (good) {
debug(TAG, "sending delivery receipt");
// got XEP-0184 request, send receipt
sendReceipt(smackMessage);
session.onReceiptsExpected();
} else {
debug(TAG, "not sending delivery receipt due to processing error");
}
} else if (!good) {
debug(TAG, "packet processing error");
}
}
}
}, new PacketTypeFilter(org.jivesoftware.smack.packet.Message.class));
mConnection.addPacketListener(new PacketListener() {
@Override
public void processPacket(Packet packet) {
org.jivesoftware.smack.packet.Presence presence = (org.jivesoftware.smack.packet.Presence) packet;
handlePresenceChanged(presence);
}
}, new PacketTypeFilter(org.jivesoftware.smack.packet.Presence.class));
ConnectionListener connectionListener = new ConnectionListener() {
/**
* Called from smack when connect() is fully successful
*
* This is called on the executor thread while we are in reconnect()
*/
@Override
public void reconnectionSuccessful() {
if (mStreamHandler == null || !mStreamHandler.isResumePending()) {
debug(TAG, "Reconnection success");
onReconnectionSuccessful();
} else {
debug(TAG, "Ignoring reconnection callback due to pending resume");
}
}
@Override
public void reconnectionFailed(Exception e) {
// We are not using the reconnection manager
throw new UnsupportedOperationException();
}
@Override
public void reconnectingIn(int seconds) {
// We are not using the reconnection manager
throw new UnsupportedOperationException();
}
@Override
public void connectionClosedOnError(final Exception e) {
/*
* This fires when:
* - Packet reader or writer detect an error
* - Stream compression failed
* - TLS fails but is required
* - Network error
* - We forced a socket shutdown
*/
debug(TAG, "reconnect on error: " + e.getMessage());
if (e.getMessage().contains("conflict")) {
execute(new Runnable() {
@Override
public void run() {
disconnect();
disconnected(new ImErrorInfo(ImpsErrorInfo.ALREADY_LOGGED,
"logged in from another location"));
}
});
} else if (!mNeedReconnect) {
execute(new Runnable() {
@Override
public void run() {
if (getState() == LOGGED_IN)
setState(LOGGING_IN,
new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, e.getMessage()));
maybe_reconnect();
}
});
}
}
@Override
public void connectionClosed() {
debug(TAG, "connection closed");
/*
* This can be called in these cases:
* - Connection is shutting down
* - because we are calling disconnect
* - in do_logout
*
* - NOT
* - because server disconnected "normally"
* - we were trying to log in (initConnection), but are failing
* - due to network error
* - due to login failing
*/
}
};
mConnection.addConnectionListener(connectionListener);
mStreamHandler = new XmppStreamHandler(mConnection, connectionListener);
mConnection.connect();
}
private void sendPresencePacket() {
org.jivesoftware.smack.packet.Presence presence = makePresencePacket(mUserPresence);
mConnection.sendPacket(presence);
}
public void sendReceipt(org.jivesoftware.smack.packet.Message msg) {
debug(TAG, "sending XEP-0184 ack to " + msg.getFrom() + " id=" + msg.getPacketID());
org.jivesoftware.smack.packet.Message ack = new org.jivesoftware.smack.packet.Message(
msg.getFrom(), msg.getType());
ack.addExtension(new DeliveryReceipts.DeliveryReceipt(msg.getPacketID()));
mConnection.sendPacket(ack);
}
public synchronized X509TrustManager getTrustManager ()
{
if (mTrustManager == null)
{
PinningTrustManager trustPinning = new PinningTrustManager(SystemKeyStore.getInstance(aContext),XMPPCertPins.PINLIST, 0);
mTrustManager = new MemorizingTrustManager(aContext, trustPinning, null);
}
return mTrustManager;
}
protected static int parsePresence(org.jivesoftware.smack.packet.Presence presence) {
int type = Presence.AVAILABLE;
Mode rmode = presence.getMode();
Type rtype = presence.getType();
if (rmode == Mode.away || rmode == Mode.xa)
type = Presence.AWAY;
else if (rmode == Mode.dnd)
type = Presence.DO_NOT_DISTURB;
else if (rtype == Type.unavailable || rtype == Type.error)
type = Presence.OFFLINE;
return type;
}
// We must release resources here, because we will not be reused
void disconnected(ImErrorInfo info) {
debug(TAG, "disconnected");
join();
setState(DISCONNECTED, info);
}
@Override
public void logoutAsync() {
execute(new Runnable() {
@Override
public void run() {
do_logout();
}
});
}
// Force immediate logout
public void logout() {
do_logout();
}
// Usually runs in executor thread, unless called from logout()
private void do_logout() {
Log.w(TAG, "logout");
setState(LOGGING_OUT, null);
disconnect();
disconnected(null);
}
// Runs in executor thread
private void disconnect() {
clearPing();
XMPPConnection conn = mConnection;
mConnection = null;
try {
conn.disconnect();
} catch (Throwable th) {
// ignore
}
mNeedReconnect = false;
mRetryLogin = false;
}
@Override
public void reestablishSessionAsync(Map<String, String> sessionContext) {
execute(new Runnable() {
@Override
public void run() {
if (getState() == SUSPENDED) {
debug(TAG, "reestablish");
setState(LOGGING_IN, null);
maybe_reconnect();
}
}
});
}
@Override
public void suspend() {
execute(new Runnable() {
@Override
public void run() {
debug(TAG, "suspend");
setState(SUSPENDED, null);
mNeedReconnect = false;
clearPing();
// Do not try to reconnect anymore if we were asked to suspend
mStreamHandler.quickShutdown();
}
});
}
private ChatSession findOrCreateSession(String address) {
ChatSession session = mSessionManager.findSession(address);
if (session == null) {
Contact contact = findOrCreateContact(address);
session = mSessionManager.createChatSession(contact);
}
return session;
}
Contact findOrCreateContact(String address) {
Contact contact = mContactListManager.getContact(address);
if (contact == null) {
contact = makeContact(address);
}
return contact;
}
private Contact makeContact(String address) {
Contact contact = null;
//load from roster if we don't have the contact
RosterEntry rEntry = null;
if (mConnection != null)
rEntry = mConnection.getRoster().getEntry(address);
if (rEntry != null)
{
XmppAddress xAddress = new XmppAddress(rEntry.getUser());
String name = rEntry.getName();
if (name == null)
name = xAddress.getUser();
contact = new Contact(xAddress, name);
}
else
{
XmppAddress xAddress = new XmppAddress(address);
contact = new Contact(xAddress, xAddress.getUser());
}
return contact;
}
private final class XmppChatSessionManager extends ChatSessionManager {
@Override
public void sendMessageAsync(ChatSession session, Message message) {
String chatRoomJid = message.getTo().getAddress();
MultiUserChat muc = ((XmppChatGroupManager)getChatGroupManager()).getMultiUserChat(chatRoomJid);
if (muc != null)
{
org.jivesoftware.smack.packet.Message msg = muc.createMessage();
msg.setBody(message.getBody());
message.setID(msg.getPacketID());
sendPacket(msg);
}
else
{
org.jivesoftware.smack.packet.Message msg = new org.jivesoftware.smack.packet.Message(
message.getTo().getAddress(), org.jivesoftware.smack.packet.Message.Type.chat);
msg.addExtension(new DeliveryReceipts.DeliveryReceiptRequest());
msg.setBody(message.getBody());
// debug(TAG, "sending packet ID " + msg.getPacketID());
message.setID(msg.getPacketID());
sendPacket(msg);
}
}
ChatSession findSession(String address) {
String nAddress = Address.stripResource(address);
for (Iterator<ChatSession> iter = mSessions.iterator(); iter.hasNext();) {
ChatSession session = iter.next();
String tAddress = Address.stripResource(session.getParticipant().getAddress().getAddress());
if (tAddress.equalsIgnoreCase(nAddress))
return session;
}
return null;
}
}
public ChatSession findSession(String address) {
return mSessionManager.findSession(address);
}
public ChatSession createChatSession(Contact contact) {
return mSessionManager.createChatSession(contact);
}
public class XmppContactListManager extends ContactListManager {
//private Hashtable<String, org.jivesoftware.smack.packet.Presence> unprocdPresence = new Hashtable<String, org.jivesoftware.smack.packet.Presence>();
@Override
protected void setListNameAsync(final String name, final ContactList list) {
execute(new Runnable() {
@Override
public void run() {
do_setListName(name, list);
}
});
}
// Runs in executor thread
private void do_setListName(String name, ContactList list) {
debug(TAG, "set list name");
mConnection.getRoster().getGroup(list.getName()).setName(name);
notifyContactListNameUpdated(list, name);
}
@Override
public String normalizeAddress(String address) {
return address.split("/")[0];
}
@Override
public void loadContactListsAsync() {
execute(new Runnable() {
@Override
public void run() {
do_loadContactLists();
}
});
}
// For testing
/*
public void loadContactLists() {
do_loadContactLists();
}*/
/**
* Create new list of contacts from roster entries.
*
* Runs in executor thread
*
* @param entryIter iterator of roster entries to add to contact list
* @param skipList list of contacts which should be omitted; new
* contacts are added to this list automatically
* @return contacts from roster which were not present in skiplist.
*/
/*
private Collection<Contact> fillContacts(Collection<RosterEntry> entryIter,
Set<String> skipList) {
Roster roster = mConnection.getRoster();
Collection<Contact> contacts = new ArrayList<Contact>();
for (RosterEntry entry : entryIter) {
String address = entry.getUser();
if (skipList != null && !skipList.add(address))
continue;
String name = entry.getName();
if (name == null)
name = address;
XmppAddress xaddress = new XmppAddress(address);
org.jivesoftware.smack.packet.Presence presence = roster.getPresence(address);
String status = presence.getStatus();
String resource = null;
Presence p = new Presence(parsePresence(presence), status,
null, null, Presence.CLIENT_TYPE_DEFAULT);
String from = presence.getFrom();
if (from != null && from.lastIndexOf("/") > 0) {
resource = from.substring(from.lastIndexOf("/") + 1);
if (resource.indexOf('.')!=-1)
resource = resource.substring(0,resource.indexOf('.'));
p.setResource(resource);
}
Contact contact = mContactListManager.getContact(xaddress.getBareAddress());
if (contact == null)
contact = new Contact(xaddress, name);
contact.setPresence(p);
contacts.add(contact);
}
return contacts;
}
*/
// Runs in executor thread
private synchronized void do_loadContactLists() {
debug(TAG, "load contact lists");
if (mConnection == null)
return;
Roster roster = mConnection.getRoster();
//Set<String> seen = new HashSet<String>();
// This group will also contain all the unfiled contacts. We will create it locally if it
// does not exist.
/*
String generalGroupName = mContext.getString(R.string.buddies);
for (Iterator<RosterGroup> giter = roster.getGroups().iterator(); giter.hasNext();) {
RosterGroup group = giter.next();
debug(TAG, "loading group: " + group.getName() + " size:" + group.getEntryCount());
Collection<Contact> contacts = fillContacts(group.getEntries(), null);
if (group.getName().equals(generalGroupName) && roster.getUnfiledEntryCount() > 0) {
Collection<Contact> unfiled = fillContacts(roster.getUnfiledEntries(), null);
contacts.addAll(unfiled);
}
XmppAddress groupAddress = new XmppAddress(group.getName());
ContactList cl = new ContactList(groupAddress, group.getName(), group
.getName().equals(generalGroupName), contacts, this);
notifyContactListCreated(cl);
notifyContactsPresenceUpdated(contacts.toArray(new Contact[contacts.size()]));
}
Collection<Contact> contacts;
if (roster.getUnfiledEntryCount() > 0) {
contacts = fillContacts(roster.getUnfiledEntries(), null);
} else {
contacts = new ArrayList<Contact>();
}
ContactList cl = getContactList(generalGroupName);
cl = new ContactList(groupAddress, group.getName(), group
.getName().equals(generalGroupName), contacts, this);
// We might have already created the Buddies contact list above
if (cl == null) {
cl = new ContactList(mUser.getAddress(), generalGroupName, true, contacts, this);
notifyContactListCreated(cl);
notifyContactsPresenceUpdated(contacts.toArray(new Contact[contacts.size()]));
}
*/
//since we don't show lists anymore, let's just load all entries together
ContactList cl;
try {
cl = mContactListManager.getDefaultContactList();
} catch (ImException e1) {
debug(TAG,"couldn't read default list");
cl = null;
}
if (cl == null)
{
String generalGroupName = mContext.getString(R.string.buddies);
Collection<Contact> contacts = new ArrayList<Contact>();
XmppAddress groupAddress = new XmppAddress(generalGroupName);
cl = new ContactList(groupAddress,generalGroupName, true, contacts, this);
notifyContactListCreated(cl);
}
for (RosterEntry rEntry : roster.getEntries())
{
String address = rEntry.getUser();
String name = rEntry.getName();
if (mUser.getAddress().getBareAddress().equals(address)) //don't load a roster for yourself
continue;
Contact contact = mContactListManager.getContact(address);
if (contact == null)
{
XmppAddress xAddr = new XmppAddress(address);
if (name == null || name.length() == 0)
name = xAddr.getUser();
contact = new Contact(xAddr,name);
}
org.jivesoftware.smack.packet.Presence p = roster.getPresence(contact.getAddress().getBareAddress());
contact.setPresence(new Presence(parsePresence(p), p.getStatus(), null, null,Presence.CLIENT_TYPE_DEFAULT));
if (!cl.containsContact(contact))
{
try {
cl.addExistingContact(contact);
} catch (ImException e) {
debug(TAG,"could not add contact to list: " + e.getLocalizedMessage());
}
}
}
notifyContactsPresenceUpdated(cl.getContacts().toArray(new Contact[cl.getContacts().size()]));
notifyContactListLoaded(cl);
notifyContactListsLoaded();
}
/*
* iterators through a list of contacts to see if there were any Presence
* notifications sent before the contact was loaded
*/
/*
private void processQueuedPresenceNotifications (Collection<Contact> contacts)
{
Roster roster = mConnection.getRoster();
//now iterate through the list of queued up unprocessed presence changes
for (Contact contact : contacts)
{
String address = parseAddressBase(contact.getAddress().getFullName());
org.jivesoftware.smack.packet.Presence presence = roster.getPresence(address);
if (presence != null)
{
debug(TAG, "processing queued presence: " + address + " - " + presence.getStatus());
unprocdPresence.remove(address);
contact.setPresence(new Presence(parsePresence(presence), presence.getStatus(), null, null, Presence.CLIENT_TYPE_DEFAULT));
Contact[] updatedContact = {contact};
notifyContactsPresenceUpdated(updatedContact);
}
}
}*/
public void listenToRoster(final Roster roster) {
roster.addRosterListener(rListener);
}
RosterListener rListener = new RosterListener() {
@Override
public void presenceChanged(org.jivesoftware.smack.packet.Presence presence) {
handlePresenceChanged(presence);
//we are already monitoring all presence packets so this is over kill
}
@Override
public void entriesUpdated(Collection<String> addresses) {
// execute(new UpdateContactsRunnable(XmppContactList.this,addresses));
loadContactListsAsync();
}
@Override
public void entriesDeleted(Collection<String> addresses) {
//LogCleaner.debug(ImApp.LOG_TAG, "entries deleted notification: " + addresses.size());
ContactList cl;
try {
cl = mContactListManager.getDefaultContactList();
for (String address : addresses)
{
Contact contact = makeContact(address);
mContactListManager.notifyContactListUpdated(cl, 1, contact);
}
} catch (ImException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//loadContactListsAsync();
}
@Override
public void entriesAdded(Collection<String> addresses) {
// LogCleaner.debug(ImApp.LOG_TAG, "entries added notification: " + addresses.size());
//execute(new UpdateContactsRunnable(XmppContactList.this,addresses));
loadContactListsAsync();
}
};
class UpdateContactsRunnable implements Runnable {
private ContactListManager mConMgr;
private Collection<String> mAddresses;
public UpdateContactsRunnable (ContactListManager conMgr, Collection<String> addresses)
{
mConMgr = conMgr;
mAddresses = addresses;
}
public void run ()
{
Collection<Contact> contacts = new ArrayList<Contact>();
for (String address :mAddresses)
contacts.add(findOrCreateContact(address));
mConMgr.notifyContactsPresenceUpdated(contacts.toArray(new Contact[contacts.size()]));
// LogCleaner.debug(ImApp.LOG_TAG, "entries updated notification: " +contacts.size());
}
}
@Override
protected ImConnection getConnection() {
return XmppConnection.this;
}
@Override
protected void doRemoveContactFromListAsync(Contact contact, ContactList list) {
// FIXME synchronize this to executor thread
if (mConnection == null)
return;
Roster roster = mConnection.getRoster();
String address = contact.getAddress().getAddress();
try {
RosterGroup group = roster.getGroup(list.getName());
if (group == null) {
debug(TAG, "could not find group " + list.getName() + " in roster");
return;
}
RosterEntry entry = roster.getEntry(address);
if (entry == null) {
debug(TAG, "could not find entry " + address + " in group " + list.getName());
//just ignore it then
}
else
{
// Remove from Roster if this is the last group
if (entry.getGroups().size() <= 1)
roster.removeEntry(entry);
group.removeEntry(entry);
}
} catch (XMPPException e) {
debug(TAG, "remove entry failed: " + e.getMessage());
throw new RuntimeException(e);
}
//otherwise, send unsub message and delete from local contact database
org.jivesoftware.smack.packet.Presence response = new org.jivesoftware.smack.packet.Presence(
org.jivesoftware.smack.packet.Presence.Type.unsubscribed);
response.setTo(address);
sendPacket(response);
notifyContactListUpdated(list, ContactListListener.LIST_CONTACT_REMOVED, contact);
}
@Override
protected void doDeleteContactListAsync(ContactList list) {
// TODO delete contact list
debug(TAG, "delete contact list " + list.getName());
}
@Override
protected void doCreateContactListAsync(String name, Collection<Contact> contacts,
boolean isDefault) {
// TODO create contact list
debug(TAG, "create contact list " + name + " default " + isDefault);
}
@Override
protected void doBlockContactAsync(String address, boolean block) {
// TODO block contact
}
@Override
protected void doAddContactToListAsync(String address, ContactList list) throws ImException {
debug(TAG, "add contact to " + list.getName());
org.jivesoftware.smack.packet.Presence response = new org.jivesoftware.smack.packet.Presence(
org.jivesoftware.smack.packet.Presence.Type.subscribed);
response.setTo(address);
sendPacket(response);
Roster roster = mConnection.getRoster();
String[] groups = new String[] { list.getName() };
try {
Contact contact = makeContact(address);
roster.createEntry(address, contact.getName(), groups);
// If contact exists locally, don't create another copy
if (!list.containsContact(contact))
notifyContactListUpdated(list, ContactListListener.LIST_CONTACT_ADDED, contact);
else
debug(TAG, "skip adding existing contact locally " + contact.getName());
} catch (XMPPException e) {
throw new RuntimeException(e);
}
}
@Override
public void declineSubscriptionRequest(String contact) {
debug(TAG, "decline subscription");
org.jivesoftware.smack.packet.Presence response = new org.jivesoftware.smack.packet.Presence(
org.jivesoftware.smack.packet.Presence.Type.unsubscribed);
response.setTo(contact);
sendPacket(response);
mContactListManager.getSubscriptionRequestListener().onSubscriptionDeclined(contact);
}
@Override
public void approveSubscriptionRequest(String contact) {
debug(TAG, "approve subscription");
try {
mContactListManager.doAddContactToListAsync(contact, getDefaultContactList());
} catch (ImException e) {
debug(TAG, "failed to add " + contact + " to default list");
}
mContactListManager.getSubscriptionRequestListener().onSubscriptionApproved(contact);
}
@Override
public Contact createTemporaryContact(String address) {
debug(TAG, "create temporary " + address);
return makeContact(address);
}
}
public void sendHeartbeat(final long heartbeatInterval) {
// Don't let heartbeats queue up if we have long running tasks - only
// do the heartbeat if executor is idle.
boolean success = executeIfIdle(new Runnable() {
@Override
public void run() {
debug(TAG, "heartbeat state = " + getState());
doHeartbeat(heartbeatInterval);
}
});
if (!success) {
debug(TAG, "failed to schedule heartbeat state = " + getState());
}
}
// Runs in executor thread
public void doHeartbeat(long heartbeatInterval) {
heartbeatSequence++;
if (mConnection == null && mRetryLogin) {
debug(TAG, "reconnect with login");
do_login();
}
if (mConnection == null)
return;
if (getState() == SUSPENDED) {
debug(TAG, "heartbeat during suspend");
return;
}
if (mNeedReconnect) {
reconnect();
} else if (!mConnection.isConnected() && getState() == LOGGED_IN) {
// Smack failed to tell us about a disconnect
Log.w(TAG, "reconnect on unreported state change");
setState(LOGGING_IN, new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, "network disconnected"));
force_reconnect();
} else if (getState() == LOGGED_IN) {
if (PING_ENABLED) {
// Check ping on every heartbeat. checkPing() will return true immediately if we already checked.
if (!checkPing()) {
Log.w(TAG, "reconnect on ping failed");
setState(LOGGING_IN, new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, "network timeout"));
force_reconnect();
} else {
// Send pings only at intervals configured by the user
if (heartbeatSequence >= heartbeatInterval) {
heartbeatSequence = 0;
debug(TAG, "ping");
sendPing();
}
}
}
}
}
private void clearPing() {
debug(TAG, "clear ping");
mPingCollector = null;
heartbeatSequence = 0;
}
// Runs in executor thread
private void sendPing() {
IQ req = new IQ() {
public String getChildElementXML() {
return "<ping xmlns='urn:xmpp:ping'/>";
}
};
req.setType(IQ.Type.GET);
PacketFilter filter = new AndFilter(new PacketIDFilter(req.getPacketID()),
new PacketTypeFilter(IQ.class));
mPingCollector = mConnection.createPacketCollector(filter);
mConnection.sendPacket(req);
}
// Runs in executor thread
private boolean checkPing() {
if (mPingCollector != null) {
IQ result = (IQ) mPingCollector.pollResult();
mPingCollector.cancel();
mPingCollector = null;
if (result == null) {
Log.e(TAG, "ping timeout");
return false;
}
}
return true;
}
// watch out, this is a different XMPPConnection class than XmppConnection! ;)
// org.jivesoftware.smack.XMPPConnection
// - vs -
// info.guardianproject.otr.app.im.plugin.xmpp.XmppConnection
public static class MyXMPPConnection extends XMPPConnection {
public MyXMPPConnection(ConnectionConfiguration config) {
super(config);
//this.getConfiguration().setSocketFactory(arg0)
}
public void shutdown() {
try {
// Be forceful in shutting down since SSL can get stuck
try {
socket.shutdownInput();
} catch (Exception e) {
}
socket.close();
shutdown(new org.jivesoftware.smack.packet.Presence(
org.jivesoftware.smack.packet.Presence.Type.unavailable));
} catch (Exception e) {
Log.e(TAG, "error on shutdown()", e);
}
}
}
@Override
public void networkTypeChanged() {
super.networkTypeChanged();
}
/*
* Force a shutdown and reconnect, unless we are already reconnecting.
*
* Runs in executor thread
*/
private void force_reconnect() {
debug(TAG, "force_reconnect need=" + mNeedReconnect);
if (mConnection == null)
return;
if (mNeedReconnect)
return;
mNeedReconnect = true;
try {
if (mConnection != null && mConnection.isConnected()) {
mStreamHandler.quickShutdown();
}
} catch (Exception e) {
Log.w(TAG, "problem disconnecting on force_reconnect: " + e.getMessage());
}
reconnect();
}
/*
* Reconnect unless we are already in the process of doing so.
*
* Runs in executor thread.
*/
private void maybe_reconnect() {
debug(TAG, "maybe_reconnect mNeedReconnect=" + mNeedReconnect + " state=" + getState()
+ " connection?=" + (mConnection != null));
// This is checking whether we are already in the process of reconnecting. If we are,
// doHeartbeat will take care of reconnecting.
if (mNeedReconnect)
return;
if (getState() == SUSPENDED)
return;
if (mConnection == null)
return;
mNeedReconnect = true;
reconnect();
}
/*
* Retry connecting
*
* Runs in executor thread
*/
private void reconnect() {
if (getState() == SUSPENDED) {
debug(TAG, "reconnect during suspend, ignoring");
return;
}
try {
Thread.sleep(2000); // Wait for network to settle
} catch (InterruptedException e) { /* ignore */
}
if (mConnection != null) {
// It is safe to ask mConnection whether it is connected, because either:
// - We detected an error using ping and called force_reconnect, which did a shutdown
// - Smack detected an error, so it knows it is not connected
// so there are no cases where mConnection can be confused about being connected here.
// The only left over cases are reconnect() being called too many times due to errors
// reported multiple times or errors reported during a forced reconnect.
// The analysis above is incorrect in the case where Smack loses connectivity
// while trying to log in. This case is handled in a future heartbeat
// by checking ping responses.
if (mConnection.isConnected()) {
Log.w(TAG, "reconnect while already connected, assuming good");
mNeedReconnect = false;
setState(LOGGED_IN, null);
return;
}
Log.i(TAG, "reconnect");
clearPing();
try {
if (mStreamHandler.isResumePossible()) {
// Connect without binding, will automatically trigger a resume
debug(TAG, "resume");
mConnection.connect(false);
initServiceDiscovery();
} else {
//mConnection.disconnect();
mConnection = null;
do_login();
/*
debug(TAG, "no resume");
mConnection.connect();
if (!mConnection.isAuthenticated()) {
// This can happen if a reconnect failed and the smack connection now has wasAuthenticated = false.
// It can also happen if auth exception was swallowed by smack.
// Try to login manually.
// Log.e(TAG, "authentication did not happen in connect() - login manually");
// mConnection.login(mUsername, mPassword, mResource);
// Make sure
if (!mConnection.isAuthenticated())
throw new XMPPException("manual auth failed");
// Manually set the state since manual auth doesn't notify listeners
mNeedReconnect = false;
setState(LOGGED_IN, null);
}
mStreamHandler.notifyInitialLogin();
initServiceDiscovery();
sendPresencePacket();
*/
}
} catch (Exception e) {
mStreamHandler.quickShutdown();
Log.w(TAG, "reconnection attempt failed", e);
// Smack incorrectly notified us that reconnection was successful, reset in case it fails
mNeedReconnect = true;
setState(LOGGING_IN, new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, e.getMessage()));
}
} else {
mNeedReconnect = true;
debug(TAG, "reconnection on network change failed");
setState(LOGGING_IN, new ImErrorInfo(ImErrorInfo.NETWORK_ERROR,
"reconnection on network change failed"));
}
}
@Override
protected void setState(int state, ImErrorInfo error) {
debug(TAG, "setState to " + state);
super.setState(state, error);
}
public void debug(String tag, String msg) {
// if (Log.isLoggable(TAG, Log.DEBUG)) {
if (Debug.DEBUG_ENABLED) {
Log.d(tag, "" + mGlobalId + " : " + msg);
}
}
@Override
public void handle(Callback[] arg0) throws IOException {
for (Callback cb : arg0) {
debug(TAG, cb.toString());
}
}
/*
public class MySASLDigestMD5Mechanism extends SASLMechanism
{
public MySASLDigestMD5Mechanism(SASLAuthentication saslAuthentication)
{
super(saslAuthentication);
}
protected void authenticate()
throws IOException, XMPPException
{
String mechanisms[] = {
getName()
};
java.util.Map props = new HashMap();
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", hostname, props, this);
super.authenticate();
}
public void authenticate(String username, String host, String password)
throws IOException, XMPPException
{
authenticationId = username;
this.password = password;
hostname = host;
String mechanisms[] = {
getName()
};
java.util.Map props = new HashMap();
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, this);
super.authenticate();
}
public void authenticate(String username, String host, CallbackHandler cbh)
throws IOException, XMPPException
{
String mechanisms[] = {
getName()
};
java.util.Map props = new HashMap();
sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh);
super.authenticate();
}
protected String getName()
{
return "DIGEST-MD5";
}
public void challengeReceived(String challenge)
throws IOException
{
//StringBuilder stanza = new StringBuilder();
byte response[];
if(challenge != null)
response = sc.evaluateChallenge(Base64.decode(challenge));
else
//response = sc.evaluateChallenge(null);
response = sc.evaluateChallenge(new byte[0]);
//String authenticationText = "";
Packet responseStanza;
//if(response != null)
//{
//authenticationText = Base64.encodeBytes(response, 8);
//if(authenticationText.equals(""))
//authenticationText = "=";
if (response == null){
responseStanza = new Response();
} else {
responseStanza = new Response(Base64.encodeBytes(response,Base64.DONT_BREAK_LINES));
}
//}
//stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
//stanza.append(authenticationText);
//stanza.append("</response>");
//getSASLAuthentication().send(stanza.toString());
getSASLAuthentication().send(responseStanza);
}
}
*/
private void initServiceDiscovery() {
debug(TAG, "init service discovery");
// register connection features
ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(mConnection);
if (sdm == null)
sdm = new ServiceDiscoveryManager(mConnection);
if (!sdm.includesFeature(DISCO_FEATURE))
sdm.addFeature(DISCO_FEATURE);
if (!sdm.includesFeature(DeliveryReceipts.NAMESPACE))
sdm.addFeature(DeliveryReceipts.NAMESPACE);
}
private void onReconnectionSuccessful() {
mNeedReconnect = false;
setState(LOGGED_IN, null);
}
private void addProviderManagerExtensions ()
{
ProviderManager pm = ProviderManager.getInstance();
// Private Data Storage
pm.addIQProvider("query","jabber:iq:private", new PrivateDataManager.PrivateDataIQProvider());
// Time
try {
pm.addIQProvider("query","jabber:iq:time", Class.forName("org.jivesoftware.smackx.packet.Time"));
} catch (ClassNotFoundException e) {
Log.w("TestClient", "Can't load class for org.jivesoftware.smackx.packet.Time");
}
// Roster Exchange
pm.addExtensionProvider("x","jabber:x:roster", new RosterExchangeProvider());
// Message Events
pm.addExtensionProvider("x","jabber:x:event", new MessageEventProvider());
// Chat State
pm.addExtensionProvider("active","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
pm.addExtensionProvider("composing","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
pm.addExtensionProvider("paused","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
pm.addExtensionProvider("inactive","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
pm.addExtensionProvider("gone","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
// XHTML
pm.addExtensionProvider("html","http://jabber.org/protocol/xhtml-im", new XHTMLExtensionProvider());
// Group Chat Invitations
pm.addExtensionProvider("x","jabber:x:conference", new GroupChatInvitation.Provider());
// Service Discovery # Items
pm.addIQProvider("query","http://jabber.org/protocol/disco#items", new DiscoverItemsProvider());
// Service Discovery # Info
pm.addIQProvider("query","http://jabber.org/protocol/disco#info", new DiscoverInfoProvider());
// Data Forms
pm.addExtensionProvider("x","jabber:x:data", new DataFormProvider());
// MUC User
pm.addExtensionProvider("x","http://jabber.org/protocol/muc#user", new MUCUserProvider());
// MUC Admin
pm.addIQProvider("query","http://jabber.org/protocol/muc#admin", new MUCAdminProvider());
// MUC Owner
pm.addIQProvider("query","http://jabber.org/protocol/muc#owner", new MUCOwnerProvider());
// Delayed Delivery
pm.addExtensionProvider("x","jabber:x:delay", new DelayInformationProvider());
// Version
try {
pm.addIQProvider("query","jabber:iq:version", Class.forName("org.jivesoftware.smackx.packet.Version"));
} catch (ClassNotFoundException e) {
// Not sure what's happening here.
}
// VCard
pm.addIQProvider("vCard","vcard-temp", new VCardProvider());
// Offline Message Requests
pm.addIQProvider("offline","http://jabber.org/protocol/offline", new OfflineMessageRequest.Provider());
// Offline Message Indicator
pm.addExtensionProvider("offline","http://jabber.org/protocol/offline", new OfflineMessageInfo.Provider());
// Last Activity
pm.addIQProvider("query","jabber:iq:last", new LastActivity.Provider());
// User Search
pm.addIQProvider("query","jabber:iq:search", new UserSearch.Provider());
// SharedGroupsInfo
pm.addIQProvider("sharedgroup","http://www.jivesoftware.org/protocol/sharedgroup", new SharedGroupsInfo.Provider());
// JEP-33: Extended Stanza Addressing
pm.addExtensionProvider("addresses","http://jabber.org/protocol/address", new MultipleAddressesProvider());
// FileTransfer
pm.addIQProvider("si","http://jabber.org/protocol/si", new StreamInitiationProvider());
pm.addIQProvider("query","http://jabber.org/protocol/bytestreams", new BytestreamsProvider());
// Privacy
pm.addIQProvider("query","jabber:iq:privacy", new PrivacyProvider());
pm.addIQProvider("command", "http://jabber.org/protocol/commands", new AdHocCommandDataProvider());
pm.addExtensionProvider("malformed-action", "http://jabber.org/protocol/commands", new AdHocCommandDataProvider.MalformedActionError());
pm.addExtensionProvider("bad-locale", "http://jabber.org/protocol/commands", new AdHocCommandDataProvider.BadLocaleError());
pm.addExtensionProvider("bad-payload", "http://jabber.org/protocol/commands", new AdHocCommandDataProvider.BadPayloadError());
pm.addExtensionProvider("bad-sessionid", "http://jabber.org/protocol/commands", new AdHocCommandDataProvider.BadSessionIDError());
pm.addExtensionProvider("session-expired", "http://jabber.org/protocol/commands", new AdHocCommandDataProvider.SessionExpiredError());
}
class NameSpace {
public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info";
public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
public static final String IQ_GATEWAY = "jabber:iq:gateway";
public static final String IQ_GATEWAY_REGISTER = "jabber:iq:gateway:register";
public static final String IQ_LAST = "jabber:iq:last";
public static final String IQ_REGISTER = "jabber:iq:register";
public static final String IQ_REGISTERED = "jabber:iq:registered";
public static final String IQ_ROSTER = "jabber:iq:roster";
public static final String IQ_VERSION = "jabber:iq:version";
public static final String CHATSTATES = "http://jabber.org/protocol/chatstates";
public static final String XEVENT = "jabber:x:event";
public static final String XDATA = "jabber:x:data";
public static final String MUC = "http://jabber.org/protocol/muc";
public static final String MUC_USER = MUC + "#user";
public static final String MUC_ADMIN = MUC + "#admin";
public static final String SPARKNS = "http://www.jivesoftware.com/spark";
public static final String DELAY = "urn:xmpp:delay";
public static final String OFFLINE = "http://jabber.org/protocol/offline";
public static final String X_DELAY = "jabber:x:delay";
public static final String VCARD_TEMP = "vcard-temp";
public static final String VCARD_TEMP_X_UPDATE = "vcard-temp:x:update";
public static final String ATTENTIONNS = "urn:xmpp:attention:0";
}
public boolean registerAccount (Imps.ProviderSettings.QueryMap providerSettings, String username, String password) throws Exception
{
initConnection(providerSettings, username);
if (mConnection.getAccountManager().supportsAccountCreation())
{
mConnection.getAccountManager().createAccount(username, password);
return true;
}
else
{
return false;//not supported
}
}
private void handlePresenceChanged(org.jivesoftware.smack.packet.Presence presence) {
if (mConnection == null)
return; //sometimes presence changes are queued, and get called after we sign off
XmppAddress xaddress = new XmppAddress(presence.getFrom());
if (mUser.getAddress().getBareAddress().equals(xaddress.getBareAddress())) //ignore presence from yourself
return;
String status = presence.getStatus();
// Get presence from the Roster to handle priorities and such
final Roster roster = mConnection.getRoster();
if (presence.getType() != Type.subscribe && presence.getType() != Type.unsubscribe)
if (roster != null)
presence = roster.getPresence(xaddress.getBareAddress());
Contact contact = mContactListManager.getContact(xaddress.getBareAddress());
Presence p = new Presence(parsePresence(presence), status, null, null,
Presence.CLIENT_TYPE_DEFAULT);
String[] presenceParts = presence.getFrom().split("/");
if (presenceParts.length > 1)
p.setResource(presenceParts[1]);
if (contact == null) {
debug(TAG, "got presence updated for NEW user: "
+ presence.getFrom());
XmppAddress xAddr = new XmppAddress(presence.getFrom());
RosterEntry rEntry = roster.getEntry(xAddr.getBareAddress());
String name = null;
if (rEntry != null)
name = rEntry.getName();
if (name == null || name.length() == 0)
name = xAddr.getUser();
contact = new Contact(xAddr,name);
try {
if (!mContactListManager.getDefaultContactList().containsContact(contact.getAddress()))
{
mContactListManager.getDefaultContactList().addExistingContact(contact);
}
} catch (ImException e) {
debug(TAG,"unable to add new contact to default list: " + e.getLocalizedMessage());
}
} else {
debug(TAG, "Got presence update for EXISTING user: "
+ contact.getAddress().getBareAddress() + " presence:" + p.getStatus());
}
if (presence.getType() == Type.subscribe || presence.getType() == Type.subscribed) {
debug(TAG,"got subscribe request: " + presence.getFrom());
mContactListManager.getSubscriptionRequestListener().onSubScriptionRequest(
contact);
}
else if (presence.getType() == Type.unsubscribe) {
//what should we do here? remove them from our list?
debug(TAG,"got unsubscribe request: " + presence.getFrom());
}
else {
contact.setPresence(p);
Contact[] contacts = new Contact[] { contact };
mContactListManager.notifyContactsPresenceUpdated(contacts);
PacketExtension pe = presence.getExtension("x", NameSpace.VCARD_TEMP_X_UPDATE);
if (pe != null) {
DefaultPacketExtension dpe = (DefaultPacketExtension)pe;
String hash = dpe.getValue("photo");
if (hash != null)
loadVCard(mContext.getContentResolver(),contact.getAddress().getAddress(),hash);
}
}
}
}