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.ImEntity; 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.otr.app.im.service.ChatSessionAdapter; import info.guardianproject.util.DNSUtil; import info.guardianproject.util.Debug; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; 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.LinkedList; import java.util.Map; import java.util.Random; import java.util.Timer; import java.util.TimerTask; 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.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; 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.IQ; import org.jivesoftware.smack.packet.Message.Body; import org.jivesoftware.smack.packet.Packet; 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 android.accounts.AccountManager; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; import android.os.RemoteException; import android.text.TextUtils; 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; private boolean mUseTor; // watch out, this is a different XMPPConnection class than XmppConnection! ;) // Synchronized by executor thread private MyXMPPConnection mConnection; private XmppStreamHandler mStreamHandler; private Roster mRoster; 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 Timer mTimerPresence; private ProxyInfo mProxyInfo = null; private long mAccountId = -1; private long mProviderId = -1; private boolean mIsGoogleAuth = false; private final static String SSLCONTEXT_TYPE = "TLS"; private static SSLContext sslContext; private final static int SOTIMEOUT = 60000; 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; private LinkedList<String> qAvatar = new LinkedList <String>(); private LinkedList<org.jivesoftware.smack.packet.Presence> qPresence = new LinkedList<org.jivesoftware.smack.packet.Presence>(); private LinkedList<org.jivesoftware.smack.packet.Packet> qPacket = new LinkedList<org.jivesoftware.smack.packet.Packet>(); public XmppConnection(Context context) throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { super(context); synchronized (XmppConnection.class) { mGlobalId = mGlobalCount++; } Debug.onConnectionStart(); SmackConfiguration.setPacketReplyTimeout(SOTIMEOUT); // Create a single threaded executor. This will serialize actions on the underlying connection. createExecutor(); addProviderManagerExtensions(); XmppStreamHandler.addExtensionProviders(); DeliveryReceipts.addExtensionProviders(); ServiceDiscoveryManager.setIdentityName("ChatSecure"); ServiceDiscoveryManager.setIdentityType("phone"); } public void initUser(long providerId, long accountId) throws ImException { ContentResolver contentResolver = mContext.getContentResolver(); Cursor cursor = contentResolver.query(Imps.ProviderSettings.CONTENT_URI,new String[] {Imps.ProviderSettings.NAME, Imps.ProviderSettings.VALUE},Imps.ProviderSettings.PROVIDER + "=?",new String[] { Long.toString(providerId)},null); if (cursor == null) throw new ImException("unable to query settings"); Imps.ProviderSettings.QueryMap providerSettings = new Imps.ProviderSettings.QueryMap( cursor, contentResolver, providerId, false, null); mProviderId = providerId; mAccountId = accountId; mUser = makeUser(providerSettings, contentResolver); mUseTor = providerSettings.getUseTor(); providerSettings.close(); } private Contact makeUser(Imps.ProviderSettings.QueryMap providerSettings, ContentResolver contentResolver) { String userName = Imps.Account.getUserName(contentResolver, mAccountId); String domain = providerSettings.getDomain(); String xmppName = userName + '@' + domain + '/' + providerSettings.getXmppResource(); return new Contact(new XmppAddress(xmppName), userName); } private void createExecutor() { mExecutor = new ThreadPoolExecutor(1, 1, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); } private boolean execute(Runnable runnable) { if (mExecutor == null) createExecutor (); //if we disconnected, will need to recreate executor here, because join() made it null 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) { qPacket.add(packet); } void postpone(final org.jivesoftware.smack.packet.Packet packet) { if (packet instanceof org.jivesoftware.smack.packet.Message) { boolean groupChat = ((org.jivesoftware.smack.packet.Message) packet).getType().equals( org.jivesoftware.smack.packet.Message.Type.groupchat); ChatSession session = findOrCreateSession(packet.getTo(), groupChat); session.onMessagePostponed(packet.getPacketID()); } } private boolean mLoadingAvatars = false; private void loadVCardsAsync () { if (!mLoadingAvatars) { execute(new AvatarLoader()); } } private class AvatarLoader implements Runnable { @Override public void run () { mLoadingAvatars = true; ContentResolver resolver = mContext.getContentResolver(); try { while (qAvatar.size()>0) { loadVCard (resolver, qAvatar.pop(), null); } } catch (Exception e) {} mLoadingAvatars = false; } } private boolean loadVCard (ContentResolver resolver, String jid, String hash) { try { boolean loadAvatar = false; if (hash != null) loadAvatar = (!DatabaseUtils.doesAvatarHashExist(resolver, Imps.Avatars.CONTENT_URI, jid, hash)); else { loadAvatar = DatabaseUtils.hasAvatarContact(resolver, Imps.Avatars.CONTENT_URI, jid); } if (!loadAvatar) { 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); b = Bitmap.createScaledBitmap(b, ImApp.DEFAULT_AVATAR_WIDTH, ImApp.DEFAULT_AVATAR_HEIGHT, false); ByteArrayOutputStream stream = new ByteArrayOutputStream(); b.compress(Bitmap.CompressFormat.JPEG, 80, 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, XmppAddress.stripResource(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); } public void reconnectAll () { for (MultiUserChat muc : mMUCs.values()) { if (!muc.isJoined()) { try { muc.join(muc.getNickname()); } catch (XMPPException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } @Override public boolean createChatGroupAsync(String chatRoomJid, String nickname) throws Exception { if (mConnection == null || getState() != ImConnection.LOGGED_IN) return false; RoomInfo roomInfo = null; if (chatRoomJid.indexOf("@")==-1) { //let's add a host to that! Collection<String> servers = MultiUserChat.getServiceNames(mConnection); chatRoomJid += '@' + servers.iterator().next(); } 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]; if (nickname == null || nickname.length() == 0) { nickname = mUsername; } 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) { String chatRoomJid = group.getAddress().getAddress(); if (mMUCs.containsKey(chatRoomJid)) { MultiUserChat muc = mMUCs.get(chatRoomJid); muc.invite(contact.getAddress().getBareAddress(),""); } } @Override protected void removeGroupMemberAsync(ChatGroup group, Contact contact) { String chatRoomJid = group.getAddress().getAddress(); if (mMUCs.containsKey(chatRoomJid)) { MultiUserChat muc = mMUCs.get(chatRoomJid); try { muc.kickParticipant(chatRoomJid, contact.getAddress().getBareAddress()); } catch (XMPPException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public String getDefaultMultiUserChatServer () { try { if (mConnection == null) return null; Collection<String> servers = MultiUserChat.getServiceNames(mConnection); if (servers == null || servers.isEmpty()) return null; else return servers.iterator().next(); } catch (Exception e) { Log.e(ImApp.LOG_TAG,"error finding MUC",e); } return null; } @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 boolean isUsingTor() { return mUseTor; } @Override public void loginAsync(long accountId, String passwordTemp, long providerId, boolean retry) { mAccountId = accountId; mPassword = passwordTemp; mProviderId = providerId; mRetryLogin = retry; ContentResolver contentResolver = mContext.getContentResolver(); if (mPassword == null) mPassword = Imps.Account.getPassword(contentResolver, mAccountId); mIsGoogleAuth = mPassword.startsWith(GTalkOAuth2.NAME); if (mIsGoogleAuth) { mPassword = mPassword.split(":")[1]; } Cursor cursor = contentResolver.query(Imps.ProviderSettings.CONTENT_URI,new String[] {Imps.ProviderSettings.NAME, Imps.ProviderSettings.VALUE},Imps.ProviderSettings.PROVIDER + "=?",new String[] { Long.toString(mProviderId)},null); if (cursor == null) return; Imps.ProviderSettings.QueryMap providerSettings = new Imps.ProviderSettings.QueryMap( cursor, contentResolver, mProviderId, false, null); mUser = makeUser(providerSettings, contentResolver); providerSettings.close(); 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(); Cursor cursor = contentResolver.query(Imps.ProviderSettings.CONTENT_URI,new String[] {Imps.ProviderSettings.NAME, Imps.ProviderSettings.VALUE},Imps.ProviderSettings.PROVIDER + "=?",new String[] { Long.toString(mProviderId)},null); if (cursor == null) return; //not going to work Imps.ProviderSettings.QueryMap providerSettings = new Imps.ProviderSettings.QueryMap( cursor, contentResolver, mProviderId, false, null); // providerSettings is closed in initConnection(); String userName = Imps.Account.getUserName(contentResolver, mAccountId); String defaultStatus = null; mNeedReconnect = true; setState(LOGGING_IN, null); mUserPresence = new Presence(Presence.AVAILABLE, defaultStatus, Presence.CLIENT_TYPE_MOBILE); try { if (userName == null || userName.length() == 0) throw new XMPPException("empty username not allowed"); initConnectionAndLogin(providerSettings, userName); setState(LOGGED_IN, null); debug(TAG, "logged in"); mNeedReconnect = false; } catch (XMPPException e) { debug(TAG, "exception thrown on connection",e); ImErrorInfo info = new ImErrorInfo(ImErrorInfo.CANT_CONNECT_TO_SERVER, e.getMessage()); mRetryLogin = true; // our default behavior is to retry if (mConnection != null && mConnection.isConnected() && (!mConnection.isAuthenticated())) { if (mIsGoogleAuth) { debug (TAG, "google failed; may need to refresh"); String newPassword = refreshGoogleToken (userName, mPassword,providerSettings.getDomain()); if (newPassword != null) mPassword = newPassword; mRetryLogin = true; } else { debug(TAG, "not authorized - will not retry"); info = new ImErrorInfo(ImErrorInfo.INVALID_USERNAME, "invalid user/password"); mRetryLogin = false; mNeedReconnect = false; } } if (mRetryLogin && getState() != SUSPENDED) { debug(TAG, "will retry"); setState(LOGGING_IN, info); maybe_reconnect(); } else { //debug(TAG, "will not retry"); //WE MUST ALWAYS RETRY! // disconnect(); // disconnected(info); } } catch (Exception e) { debug(TAG, "login failed",e); mRetryLogin = true; mNeedReconnect = true; debug(TAG, "will retry"); ImErrorInfo info = new ImErrorInfo(ImErrorInfo.UNKNOWN_ERROR, "keymanagement exception"); setState(LOGGING_IN, info); } finally { providerSettings.close(); if (!cursor.isClosed()) cursor.close(); } } private String refreshGoogleToken (String userName, String expiredToken, String domain) { //invalidate our old one, that is locally cached AccountManager.get(mContext.getApplicationContext()).invalidateAuthToken("com.google", expiredToken); //request a new one String newToken = GTalkOAuth2.getGoogleAuthToken(userName + '@' + domain, mContext.getApplicationContext()); if (newToken != null) { //now store the new one, for future use until it expires ImApp.insertOrUpdateAccount(mContext.getContentResolver(), mProviderId, userName, GTalkOAuth2.NAME + ':' + newToken ); } return newToken; } // 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; mRoster = mConnection.getRoster(); mUser = user; setState(state, null); } private void initConnectionAndLogin (Imps.ProviderSettings.QueryMap providerSettings,String userName) throws XMPPException, KeyManagementException, NoSuchAlgorithmException, IllegalStateException, RuntimeException { Debug.onConnectionStart(); //only activates if Debug TRUE is set, so you can leave this in! initConnection(providerSettings, userName); mResource = providerSettings.getXmppResource(); //disable compression based on statement by Ge0rg mConfig.setCompressionEnabled(false); if (mConnection.isConnected()) { mConnection.login(mUsername, mPassword, mResource); String fullJid = mConnection.getUser(); XmppAddress xa = new XmppAddress(fullJid); mUser = new Contact(xa, xa.getUser()); mStreamHandler.notifyInitialLogin(); initServiceDiscovery(); sendPresencePacket(); mRoster = mConnection.getRoster(); mRoster.setSubscriptionMode(Roster.SubscriptionMode.manual); getContactListManager().listenToRoster(mRoster); } } // Runs in executor thread private void initConnection(Imps.ProviderSettings.QueryMap providerSettings, String userName) throws NoSuchAlgorithmException, KeyManagementException, XMPPException { boolean allowPlainAuth = providerSettings.getAllowPlainAuth(); boolean requireTls = providerSettings.getRequireTls(); boolean doDnsSrv = providerSettings.getDoDnsSrv(); boolean tlsCertVerify = providerSettings.getTlsCertVerify(); boolean useSASL = true;//!allowPlainAuth; String domain = providerSettings.getDomain(); mPriority = providerSettings.getXmppResourcePrio(); int serverPort = providerSettings.getPort(); String server = providerSettings.getServer(); if ("".equals(server)) server = null; debug(TAG, "TLS required? " + requireTls); debug(TAG, "cert verification? " + tlsCertVerify); if (providerSettings.getUseTor()) { 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) { //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) //if serverPort is set to 0 then use 5222 as default 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);"); //String serviceName = domain; //if (server != null && (!server.endsWith(".onion"))) //if a connect server was manually entered, and is not an .onion address // serviceName = server; 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"); SASLAuthentication.registerSASLMechanism( GTalkOAuth2.NAME, GTalkOAuth2.class ); if (mIsGoogleAuth) //if using google auth enable sasl SASLAuthentication.supportSASLMechanism( GTalkOAuth2.NAME, 0); else if (domain.contains("google.com")||domain.contains("gmail.com")) //if not google auth, disable if doing direct google auth SASLAuthentication.unsupportSASLMechanism( GTalkOAuth2.NAME); SASLAuthentication.supportSASLMechanism("PLAIN", 1); SASLAuthentication.supportSASLMechanism("DIGEST-MD5", 2); if (requireTls) { MemorizingTrustManager trustManager = ImApp.sImApp.getTrustManager(); if (sslContext == null) { sslContext = SSLContext.getInstance(SSLCONTEXT_TYPE); SecureRandom secureRandom = new java.security.SecureRandom(); sslContext.init(null, new javax.net.ssl.TrustManager[] { trustManager }, secureRandom); try { sslContext.getDefaultSSLParameters().getCipherSuites(); if (Build.VERSION.SDK_INT >= 20) { sslContext.getDefaultSSLParameters().setCipherSuites(XMPPCertPins.SSL_IDEAL_CIPHER_SUITES_API_20); } else { sslContext.getDefaultSSLParameters().setCipherSuites(XMPPCertPins.SSL_IDEAL_CIPHER_SUITES); } } catch (Exception e) { //this can happen if the cipher suites aren't available on the devices debug(TAG, "Error setting ideal cipher suites: " + e); } } int currentapiVersion = android.os.Build.VERSION.SDK_INT; if (currentapiVersion >= 16){ // Enable TLS1.2 and TLS1.1 on supported versions of android // http://stackoverflow.com/questions/16531807/android-client-server-on-tls-v1-2 //mConfig.setEnabledProtocols(new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" }); sslContext.getDefaultSSLParameters().setProtocols(new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" }); } if (currentapiVersion >= android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH){ mConfig.setEnabledCipherSuites(XMPPCertPins.SSL_IDEAL_CIPHER_SUITES); } HostnameVerifier hv = trustManager.wrapHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier()); mConfig.setHostnameVerifier(hv); mConfig.setCustomSSLContext(sslContext); mConfig.setSecurityMode(SecurityMode.required); mConfig.setVerifyChainEnabled(true); mConfig.setVerifyRootCAEnabled(true); mConfig.setExpiredCertificatesCheckEnabled(true); mConfig.setNotMatchingDomainCheckEnabled(true); mConfig.setSelfSignedCertificateEnabled(false); 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); if (sslContext == null) { sslContext = SSLContext.getInstance(SSLCONTEXT_TYPE); SecureRandom mSecureRandom = new java.security.SecureRandom(); sslContext.init(null, new javax.net.ssl.TrustManager[] { getDummyTrustManager () }, mSecureRandom); sslContext.getDefaultSSLParameters().setCipherSuites(XMPPCertPins.SSL_IDEAL_CIPHER_SUITES); } mConfig.setCustomSSLContext(sslContext); if (!allowPlainAuth) SASLAuthentication.unsupportSASLMechanism("PLAIN"); mConfig.setVerifyChainEnabled(false); mConfig.setVerifyRootCAEnabled(false); mConfig.setExpiredCertificatesCheckEnabled(false); mConfig.setNotMatchingDomainCheckEnabled(false); mConfig.setSelfSignedCertificateEnabled(true); } // 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(); if (smackMessage.getError() != null) { // smackMessage.getError().getCode(); String error = "Error " + smackMessage.getError().getCode() + " (" + smackMessage.getError().getCondition() + "): " + smackMessage.getError().getMessage(); debug (TAG, error); return; } if (body == null) { Collection<Body> mColl = smackMessage.getBodies(); for (Body bodyPart : mColl) { String msg = bodyPart.getMessage(); if (msg != null) { body = msg; break; } } } DeliveryReceipts.DeliveryReceipt drIncoming = (DeliveryReceipts.DeliveryReceipt) smackMessage .getExtension("received", DeliveryReceipts.NAMESPACE); if (drIncoming != null) { debug(TAG, "got delivery receipt for " + drIncoming.getId()); boolean groupMessage = smackMessage.getType() == org.jivesoftware.smack.packet.Message.Type.groupchat; ChatSession session = findOrCreateSession(address, groupMessage); session.onMessageReceipt(drIncoming.getId()); } if (body != null) { XmppAddress aFrom = new XmppAddress(smackMessage.getFrom()); XmppAddress aTo = new XmppAddress(smackMessage.getTo()); boolean isGroupMessage = smackMessage.getType() == org.jivesoftware.smack.packet.Message.Type.groupchat; ChatSession session = findOrCreateSession(address, isGroupMessage); Message rec = new Message(body); rec.setTo(aTo); rec.setFrom(aFrom); rec.setDateTime(new Date()); rec.setType(Imps.MessageType.INCOMING); /* // Detect if this was said by us, and mark message as outgoing if (isGroupMessage && 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; qPresence.push(presence); } }, new PacketTypeFilter(org.jivesoftware.smack.packet.Presence.class)); if (mTimerPackets == null) initPacketProcessor(); if (mTimerPresence == null) initPresenceProcessor (); 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(); mRoster = mConnection.getRoster(); } 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() { public void run() { if (getState() == LOGGED_IN) { //Thread.sleep(1000); mNeedReconnect = true; setState(LOGGING_IN, new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, "network error")); 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); for (int i = 0; i < 3; i++) { try { mConnection.connect(); break; } catch (Exception uhe) { //sometimes DNS fails.. let's wait and try again a few times try { Thread.sleep(500);} catch (Exception e){} } } if (!mConnection.isConnected()) throw new XMPPException("Unable to connect to host"); } private void sendPresencePacket() { qPacket.add(makePresencePacket(mUserPresence)); } 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())); sendPacket(ack); } public X509TrustManager getDummyTrustManager () { return new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }; } protected int parsePresence(org.jivesoftware.smack.packet.Presence presence) { int type = Imps.Presence.AVAILABLE; Mode rmode = presence.getMode(); Type rtype = presence.getType(); //if a device sends something other than available, check if there is a higher priority one available on the server /* if (rmode != Mode.available) { if (mRoster != null) { org.jivesoftware.smack.packet.Presence npresence = mRoster.getPresence(XmppAddress.stripResource(presence.getFrom())); rmode = npresence.getMode(); rtype = npresence.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; } }*/ if (rmode == Mode.chat) type = Imps.Presence.AVAILABLE; else if (rmode == Mode.away || rmode == Mode.xa) type = Imps.Presence.AWAY; else if (rmode == Mode.dnd) type = Imps.Presence.DO_NOT_DISTURB; else if (rtype == Type.unavailable || rtype == Type.error) type = Imps.Presence.OFFLINE; else if (rtype == Type.unsubscribed) type = Imps.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() { new Thread(new Runnable() { @Override public void run() { do_logout(); } }).start(); } // Force immediate logout public void logout() { logoutAsync(); } // Usually runs in executor thread, unless called from logout() private void do_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"); mNeedReconnect = false; 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 if (mStreamHandler != null) mStreamHandler.quickShutdown(); } }); } private ChatSession findOrCreateSession(String address, boolean groupChat) { ChatSession session = mSessionManager.findSession(address); if (session == null) { ImEntity participant = findOrCreateParticipant(address, groupChat); session = mSessionManager.createChatSession(participant,false); } return session; } ImEntity findOrCreateParticipant(String address, boolean groupChat) { ImEntity participant = mContactListManager.getContact(address); if (participant == null) { if (!groupChat) { participant = makeContact(address); } else { try { mChatGroupManager.createChatGroupAsync(address, mUser.getName()); Address xmppAddress = new XmppAddress(address); participant = mChatGroupManager.getChatGroup(xmppAddress); } catch (Exception e) { Log.e(ImApp.LOG_TAG,"unable to join group chat",e); } } } return participant; } Contact findOrCreateContact(String address) { return (Contact) findOrCreateParticipant(address, false); } 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(address); 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); org.jivesoftware.smack.packet.Message msgXmpp = null; if (muc != null) { msgXmpp = muc.createMessage(); } else { msgXmpp = new org.jivesoftware.smack.packet.Message( message.getTo().getAddress(), org.jivesoftware.smack.packet.Message.Type.chat); msgXmpp.addExtension(new DeliveryReceipts.DeliveryReceiptRequest()); Contact contact = mContactListManager.getContact(message.getTo().getBareAddress()); if (contact != null && contact.getPresence() !=null && (!contact.getPresence().isOnline())) requestPresenceRefresh(message.getTo().getBareAddress()); } if (message.getFrom() == null) msgXmpp.setFrom(mUser.getAddress().getAddress()); else msgXmpp.setFrom(message.getFrom().getAddress()); msgXmpp.setBody(message.getBody()); if (message.getID() != null) msgXmpp.setPacketID(message.getID()); else message.setID(msgXmpp.getPacketID()); sendPacket(msgXmpp); } ChatSession findSession(String address) { return mSessions.get(Address.stripResource(address)); } @Override public ChatSession createChatSession(ImEntity participant, boolean isNewSession) { requestPresenceRefresh(participant.getAddress().getAddress()); ChatSession session = super.createChatSession(participant,isNewSession); //do avatar check if we have a no dominant presence qAvatar.push(participant.getAddress().getAddress()); // mSessions.put(Address.stripResource(participant.getAddress().getAddress()),session); return session; } } private void requestPresenceRefresh (String address) { org.jivesoftware.smack.packet.Presence p = new org.jivesoftware.smack.packet.Presence(Type.error); p.setFrom(address); qPresence.push(p); } public class XmppContactListManager extends ContactListManager { @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.stripResource(address); } @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 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 = getContact(address); if (contact == null) { XmppAddress xAddr = new XmppAddress(address); if (name == null || name.length() == 0) name = xAddr.getUser(); contact = new Contact(xAddr,name); } requestPresenceRefresh(address); if (!cl.containsContact(contact)) { try { cl.addExistingContact(contact); } catch (ImException e) { debug(TAG,"could not add contact to list: " + e.getLocalizedMessage()); } } } notifyContactListLoaded(cl); notifyContactListsLoaded(); } // Runs in executor thread public void addContactsToList(Collection<String> addresses) { debug(TAG, "add contacts to lists"); if (mConnection == null) return; 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 (String address : addresses) { if (mUser.getAddress().getBareAddress().equals(address)) //don't load a roster for yourself continue; Contact contact = getContact(address); if (contact == null) { XmppAddress xAddr = new XmppAddress(address); contact = new Contact(xAddr,xAddr.getUser()); } //org.jivesoftware.smack.packet.Presence p = roster.getPresence(contact.getAddress().getBareAddress()); //qPresence.push(p); if (!cl.containsContact(contact)) { try { cl.addExistingContact(contact); } catch (ImException e) { debug(TAG,"could not add contact to list: " + e.getLocalizedMessage()); } } } 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) { qPresence.push(presence); } @Override public void entriesUpdated(Collection<String> addresses) { for (String address :addresses) { requestPresenceRefresh(address); } } @Override public void entriesDeleted(Collection<String> addresses) { ContactList cl; try { cl = mContactListManager.getDefaultContactList(); for (String address : addresses) { requestPresenceRefresh(address); Contact contact = mContactListManager.getContact(XmppAddress.stripResource(address)); mContactListManager.notifyContactListUpdated(cl, ContactListListener.LIST_CONTACT_REMOVED, contact); } } catch (ImException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void entriesAdded(Collection<String> addresses) { try { if (mContactListManager.getState() == LISTS_LOADED) { for (String address : addresses) { Contact contact = getContact(address); requestPresenceRefresh(address); if (contact == null) { XmppAddress xAddr = new XmppAddress(address); contact = new Contact(xAddr,xAddr.getUser()); } try { ContactList cl = mContactListManager.getDefaultContactList(); if (!cl.containsContact(contact)) cl.addExistingContact(contact); } catch (Exception e) { debug(TAG,"could not add contact to list: " + e.getLocalizedMessage()); } } } } catch (Exception e) { Log.d(ImApp.LOG_TAG,"error adding contacts",e); } } }; @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; String address = contact.getAddress().getAddress(); //otherwise, send unsub message and delete from local contact database org.jivesoftware.smack.packet.Presence presence = new org.jivesoftware.smack.packet.Presence( org.jivesoftware.smack.packet.Presence.Type.unsubscribe); presence.setTo(address); sendPacket(presence); presence = new org.jivesoftware.smack.packet.Presence( org.jivesoftware.smack.packet.Presence.Type.unsubscribed); presence.setTo(address); sendPacket(presence); try { RosterEntry entry = mRoster.getEntry(address); RosterGroup group = mRoster.getGroup(list.getName()); if (group == null) { debug(TAG, "could not find group " + list.getName() + " in roster"); if (mRoster != null) mRoster.removeEntry(entry); } else { group.removeEntry(entry); entry = mRoster.getEntry(address); // Remove from Roster if this is the last group if (entry != null && entry.getGroups() != null && entry.getGroups().size() <= 1) mRoster.removeEntry(entry); } } catch (XMPPException e) { debug(TAG, "remove entry failed: " + e.getMessage()); throw new RuntimeException(e); } 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(Contact contact, ContactList list) throws ImException { debug(TAG, "add contact to " + list.getName()); if (mConnection.isConnected()) { org.jivesoftware.smack.packet.Presence reqSubscribe = new org.jivesoftware.smack.packet.Presence( org.jivesoftware.smack.packet.Presence.Type.subscribe); reqSubscribe.setTo(contact.getAddress().getBareAddress()); sendPacket(reqSubscribe); org.jivesoftware.smack.packet.Presence reqSubscribed = new org.jivesoftware.smack.packet.Presence( org.jivesoftware.smack.packet.Presence.Type.subscribed); reqSubscribed.setTo(contact.getAddress().getBareAddress()); sendPacket(reqSubscribed); String[] groups = new String[] { list.getName() }; try { RosterEntry rEntry = mRoster.getEntry(contact.getAddress().getBareAddress()); RosterGroup rGroup = mRoster.getGroup(list.getName()); if (rGroup == null) { if (rEntry == null) mRoster.createEntry (contact.getAddress().getBareAddress(), contact.getName(), null); } else if (rEntry == null) { mRoster.createEntry(contact.getAddress().getBareAddress(), contact.getName(), groups); } } catch (XMPPException e) { debug(TAG,"error updating remote roster",e); throw new ImException("error updating remote roster"); } catch (IllegalStateException e) { String msg = "Not logged in to server while updating remote roster"; debug(TAG, msg, e); throw new ImException(msg); } do_loadContactLists(); notifyContactListUpdated(list, ContactListListener.LIST_CONTACT_ADDED, contact); } } @Override public void declineSubscriptionRequest(Contact 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.getAddress().getBareAddress()); sendPacket(response); try { mContactListManager.getSubscriptionRequestListener().onSubscriptionDeclined(contact, mProviderId, mAccountId); } catch (RemoteException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void approveSubscriptionRequest(final Contact contact) { new Thread(new Runnable() { public void run () { debug(TAG, "approve subscription: " + contact.getAddress().getAddress()); try { doAddContactToListAsync(contact, getContactListManager().getDefaultContactList()); mContactListManager.getSubscriptionRequestListener().onSubscriptionApproved(contact, mProviderId, mAccountId); } catch (ImException e) { debug (TAG, "error responding to subscription approval: " + e.getLocalizedMessage()); } catch (RemoteException e) { debug (TAG, "error responding to subscription approval: " + e.getLocalizedMessage()); } } }).start(); } @Override public Contact[] createTemporaryContacts(String[] addresses) { // debug(TAG, "create temporary " + address); Contact[] contacts = new Contact[addresses.length]; int i = 0; for (String address : addresses) { contacts[i++] = makeContact(address); } notifyContactsPresenceUpdated(contacts); return contacts; } @Override protected void doSetContactName(String address, String name) throws ImException { Roster roster = mConnection.getRoster(); RosterEntry entry = roster.getEntry(address); // confirm entry still exists if (entry == null) { return; } // set name entry.setName(name); } } 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 (getState() == SUSPENDED) { debug(TAG, "heartbeat during suspend"); return; } if (mConnection == null && mRetryLogin) { debug(TAG, "reconnect with login"); do_login(); return; } if (mConnection == null) return; if (mNeedReconnect) { reconnect(); } else if (!mConnection.isConnected() && getState() == LOGGED_IN) { // Smack failed to tell us about a disconnect debug(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()) { debug(TAG, "reconnect on ping failed: " + mUser.getAddress().getAddress()); setState(LOGGING_IN, new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, "network timeout")); maybe_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); } public void shutdown() { if (socket != null) { 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(); execute(new Runnable() { @Override public void run() { if (mState == SUSPENDED || mState == SUSPENDING) { debug(TAG, "network type changed"); mNeedReconnect = false; setState(LOGGING_IN, null); reconnect(); } } }); } /* * Force a shutdown and reconnect, unless we are already reconnecting. * * Runs in executor thread */ private void force_reconnect() { debug(TAG, "force_reconnect mNeedReconnect=" + mNeedReconnect + " state=" + getState() + " connection?=" + (mConnection != null)); 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; } 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. clearPing(); if (mConnection.isConnected()) { debug(TAG,"reconnect while already connected, assuming good: " + mConnection); mNeedReconnect = false; setState(LOGGED_IN, null); return; } debug(TAG, "reconnect"); try { if (mStreamHandler.isResumePossible()) { // Connect without binding, will automatically trigger a resume debug(TAG, "mStreamHandler resume"); mConnection.connect(false); initServiceDiscovery(); } else { debug(TAG, "reconnection on network change failed: " + mUser.getAddress().getAddress()); mConnection = null; mNeedReconnect = true; setState(LOGGING_IN, new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, null)); while (mNeedReconnect) { do_login(); if (mNeedReconnect) try { Thread.sleep(3000);} catch (Exception e){} } } } catch (Exception e) { if (mStreamHandler != null) mStreamHandler.quickShutdown(); mConnection = null; debug(TAG, "reconnection attempt failed", e); // Smack incorrectly notified us that reconnection was successful, reset in case it fails mNeedReconnect = false; setState(LOGGING_IN, new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, e.getMessage())); //while (mNeedReconnect) // do_login(); } } else { mNeedReconnect = false; mConnection = null; debug(TAG, "reconnection on network change failed"); setState(LOGGING_IN, new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, "reconnection on network change failed")); //while (mNeedReconnect) // do_login(); } } @Override protected void setState(int state, ImErrorInfo error) { debug(TAG, "setState to " + state); super.setState(state, error); if (state == LOGGED_IN) { //update and send new presence packet out mUserPresence = new Presence(Presence.AVAILABLE, "", Presence.CLIENT_TYPE_MOBILE); sendPresencePacket(); //request presence of remote contact for all existing sessions for (ChatSessionAdapter session : mSessionManager.getAdapter().getActiveChatSessions()) { requestPresenceRefresh(session.getAddress()); } mChatGroupManager.reconnectAll(); } } public void debug(String tag, String msg) { // if (Log.isLoggable(TAG, Log.DEBUG)) { if (Debug.DEBUG_ENABLED) { Log.d(tag, "" + mGlobalId + " : " + msg); } } public void debug(String tag, String msg, Exception e) { if (Debug.DEBUG_ENABLED) { Log.e(tag, "" + mGlobalId + " : " + msg,e); } } @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, Map<String,String> params) throws Exception { initConnection(providerSettings, username); if (mConnection.getAccountManager().supportsAccountCreation()) { mConnection.getAccountManager().createAccount(username, password, params); return true; } else { return false;//not supported } } private Contact handlePresenceChanged(org.jivesoftware.smack.packet.Presence presence) { if (presence == null || presence.getFrom() == null) //our presence isn't really valid return null; String from = presence.getFrom(); if (presence.getType() == Type.error) { if (mRoster == null) return null; presence = mRoster.getPresence(from); } if (TextUtils.isEmpty(from)) return null; XmppAddress xaddress = new XmppAddress(from); if (mUser.getAddress().getBareAddress().equals(xaddress.getBareAddress())) //ignore presence from yourself return null; String status = presence.getStatus(); Presence p = new Presence(parsePresence(presence), status, null, null, Presence.CLIENT_TYPE_DEFAULT); //this is only persisted in memory p.setPriority(presence.getPriority()); // Get presence from the Roster to handle priorities and such // TODO: this causes bad network and performance issues // if (presence.getType() == Type.available) //get the latest presence for the highest priority Contact contact = mContactListManager.getContact(xaddress.getBareAddress()); String[] presenceParts = presence.getFrom().split("/"); if (presenceParts.length > 1) p.setResource(presenceParts[1]); if (contact == null && presence.getType() == Type.subscribe) { XmppAddress xAddr = new XmppAddress(presence.getFrom()); if (mRoster == null) return null; RosterEntry rEntry = mRoster.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 if (contact == null) { return null; //do nothing if we don't have a contact } if (presence.getType() == Type.subscribe) { debug(TAG,"got subscribe request: " + presence.getFrom()); try { mContactListManager.getSubscriptionRequestListener().onSubScriptionRequest(contact, mProviderId, mAccountId); } catch (RemoteException e) { Log.e(TAG,"remote exception on subscription handling",e); } } else if (presence.getType() == Type.subscribed) { debug(TAG,"got subscribed confirmation request: " + presence.getFrom()); try { mContactListManager.getSubscriptionRequestListener().onSubscriptionApproved(contact, mProviderId, mAccountId); } catch (RemoteException e) { Log.e(TAG,"remote exception on subscription handling",e); } } else if (presence.getType() == Type.unsubscribe) { debug(TAG,"got unsubscribe request: " + presence.getFrom()); //TBD how to handle this // mContactListManager.getSubscriptionRequestListener().onUnSubScriptionRequest(contact); } else if (presence.getType() == Type.unsubscribed) { debug(TAG,"got unsubscribe request: " + presence.getFrom()); try { mContactListManager.getSubscriptionRequestListener().onSubscriptionDeclined(contact, mProviderId, mAccountId); } catch (RemoteException e) { Log.e(TAG,"remote exception on subscription handling",e); } } else { //this is typical presence, let's get the latest/highest priority debug(TAG,"got presence:: " + presence.getFrom() + "=" + p.getStatusText()); if (contact.getPresence() != null) { Presence pOld = contact.getPresence(); if (pOld.getResource() != null && pOld.getResource().equals(p.getResource())) //if the same resource as the existing one, then update it { contact.setPresence(p); } else if (p.getPriority() >= pOld.getPriority()) //if priority is higher, then override { contact.setPresence(p); } if (p.getStatus() != Imps.Presence.AVAILABLE) { //if offline, let's check for another online presence presence = mRoster.getPresence(presence.getFrom()); p = new Presence(parsePresence(presence), status, null, null, Presence.CLIENT_TYPE_DEFAULT); //this is only persisted in memory p.setPriority(presence.getPriority()); contact.setPresence(p); } } else { //we don't have a presence yet so set one contact.setPresence(p); } } return contact; } private void initPresenceProcessor () { mTimerPresence = new Timer(); mTimerPresence.scheduleAtFixedRate(new TimerTask() { public void run() { if (qPresence.size() > 0) { ArrayList<Contact> alUpdate = new ArrayList<Contact>(); org.jivesoftware.smack.packet.Presence p = null; Contact contact = null; while (qPresence.peek() != null) { p = qPresence.pop(); contact = handlePresenceChanged(p); if (contact != null) { alUpdate.add(contact); } } //Log.d(ImApp.LOG_TAG,"XMPP processed presence q=" + alUpdate.size()); mContactListManager.notifyContactsPresenceUpdated(alUpdate.toArray(new Contact[alUpdate.size()])); loadVCardsAsync(); } } }, 1000, 5000); } Timer mTimerPackets = null; private void initPacketProcessor () { mTimerPackets = new Timer(); mTimerPackets.scheduleAtFixedRate(new TimerTask() { public void run() { try { org.jivesoftware.smack.packet.Packet packet = null; if (qPacket.size() > 0) while (qPacket.peek()!=null) { packet = qPacket.poll(); if (mConnection == null || (!mConnection.isConnected())) { debug(TAG, "postponed packet to " + packet.getTo() + " because we are not connected"); postpone(packet); return; } try { mConnection.sendPacket(packet); } catch (IllegalStateException ex) { postpone(packet); debug(TAG, "postponed packet to " + packet.getTo() + " because socket is disconnected"); } } } catch (Exception e) { Log.e(ImApp.LOG_TAG,"error processing presence",e); } } }, 500, 500); } }