package org.yaxim.androidclient.service;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import de.duenndns.ssl.MemorizingTrustManager;
import org.jivesoftware.smack.AccountManager;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionListener;
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.SmackConfiguration;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Presence.Mode;
import org.jivesoftware.smack.parsing.ParsingExceptionCallback;
import org.jivesoftware.smack.parsing.UnparsablePacket;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.util.DNSUtil;
import org.jivesoftware.smack.util.dns.DNSJavaResolver;
import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
import org.jivesoftware.smackx.entitycaps.cache.SimpleDirectoryPersistentCache;
import org.jivesoftware.smackx.GroupChatInvitation;
import org.jivesoftware.smackx.ServiceDiscoveryManager;
import org.jivesoftware.smackx.muc.DiscussionHistory;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.muc.RoomInfo;
import org.jivesoftware.smackx.carbons.Carbon;
import org.jivesoftware.smackx.carbons.CarbonManager;
import org.jivesoftware.smackx.entitycaps.provider.CapsExtensionProvider;
import org.jivesoftware.smackx.forward.Forwarded;
import org.jivesoftware.smackx.provider.DataFormProvider;
import org.jivesoftware.smackx.provider.DelayInfoProvider;
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.packet.DelayInformation;
import org.jivesoftware.smackx.packet.DelayInfo;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.MUCUser;
import org.jivesoftware.smackx.packet.Version;
import org.jivesoftware.smackx.ping.PingManager;
import org.jivesoftware.smackx.ping.packet.*;
import org.jivesoftware.smackx.ping.provider.PingProvider;
import org.jivesoftware.smackx.receipts.DeliveryReceipt;
import org.jivesoftware.smackx.receipts.DeliveryReceiptManager;
import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest;
import org.jivesoftware.smackx.receipts.ReceiptReceivedListener;
import org.yaxim.androidclient.YaximApplication;
import org.yaxim.androidclient.data.ChatHelper;
import org.yaxim.androidclient.data.ChatProvider;
import org.yaxim.androidclient.data.ChatRoomHelper;
import org.yaxim.androidclient.data.RosterProvider;
import org.yaxim.androidclient.data.YaximConfiguration;
import org.yaxim.androidclient.data.ChatProvider.ChatConstants;
import org.yaxim.androidclient.data.RosterProvider.RosterConstants;
import org.yaxim.androidclient.exceptions.YaximXMPPException;
import org.yaxim.androidclient.packet.PreAuth;
import org.yaxim.androidclient.packet.Replace;
import org.yaxim.androidclient.util.ConnectionState;
import org.yaxim.androidclient.util.LogConstants;
import org.yaxim.androidclient.util.StatusMode;
import org.yaxim.androidclient.R;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class SmackableImp implements Smackable {
final static private String TAG = "yaxim.SmackableImp";
final static private int PACKET_TIMEOUT = 30000;
final static private String[] SEND_OFFLINE_PROJECTION = new String[] {
ChatConstants._ID, ChatConstants.JID,
ChatConstants.MESSAGE, ChatConstants.DATE, ChatConstants.PACKET_ID };
final static private String SEND_OFFLINE_SELECTION =
ChatConstants.DIRECTION + " = " + ChatConstants.OUTGOING + " AND " +
ChatConstants.DELIVERY_STATUS + " = " + ChatConstants.DS_NEW;
static final DiscoverInfo.Identity YAXIM_IDENTITY = new DiscoverInfo.Identity("client",
YaximApplication.XMPP_IDENTITY_NAME,
YaximApplication.XMPP_IDENTITY_TYPE);
static File capsCacheDir = null; ///< this is used to cache if we already initialized EntityCapsCache
static {
registerSmackProviders();
DNSUtil.setDNSResolver(DNSJavaResolver.getInstance());
// initialize smack defaults before any connections are created
SmackConfiguration.setPacketReplyTimeout(PACKET_TIMEOUT);
SmackConfiguration.setDefaultPingInterval(0);
}
static void registerSmackProviders() {
ProviderManager pm = ProviderManager.getInstance();
// add IQ handling
pm.addIQProvider("query","http://jabber.org/protocol/disco#info", new DiscoverInfoProvider());
pm.addIQProvider("query","http://jabber.org/protocol/disco#items", new DiscoverItemsProvider());
// add delayed delivery notifications
pm.addExtensionProvider("delay","urn:xmpp:delay", new DelayInfoProvider());
pm.addExtensionProvider("x","jabber:x:delay", new DelayInfoProvider());
// add XEP-0092 Software Version
pm.addIQProvider("query", Version.NAMESPACE, new Version.Provider());
// data forms
pm.addExtensionProvider("x","jabber:x:data", new DataFormProvider());
// add carbons and forwarding
pm.addExtensionProvider("forwarded", Forwarded.NAMESPACE, new Forwarded.Provider());
pm.addExtensionProvider("sent", Carbon.NAMESPACE, new Carbon.Provider());
pm.addExtensionProvider("received", Carbon.NAMESPACE, new Carbon.Provider());
// add delivery receipts
pm.addExtensionProvider(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE, new DeliveryReceipt.Provider());
pm.addExtensionProvider(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE, new DeliveryReceiptRequest.Provider());
// add XMPP Ping (XEP-0199)
pm.addIQProvider("ping","urn:xmpp:ping", new PingProvider());
ServiceDiscoveryManager.setDefaultIdentity(YAXIM_IDENTITY);
// XEP-0115 Entity Capabilities
pm.addExtensionProvider("c", "http://jabber.org/protocol/caps", new CapsExtensionProvider());
// XEP-0308 Last Message Correction
pm.addExtensionProvider("replace", Replace.NAMESPACE, new Replace.Provider());
// XEP-XXXX Pre-Authenticated Roster Subscription
pm.addExtensionProvider("preauth", PreAuth.NAMESPACE, new PreAuth.Provider());
// MUC User
pm.addExtensionProvider("x","http://jabber.org/protocol/muc#user", new MUCUserProvider());
// MUC direct invitation
pm.addExtensionProvider("x","jabber:x:conference", new GroupChatInvitation.Provider());
// 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());
XmppStreamHandler.addExtensionProviders();
}
private final YaximConfiguration mConfig;
private ConnectionConfiguration mXMPPConfig;
private XmppStreamHandler.ExtXMPPConnection mXMPPConnection;
private XmppStreamHandler mStreamHandler;
private Thread mConnectingThread;
private Object mConnectingThreadMutex = new Object();
private ConnectionState mRequestedState = ConnectionState.OFFLINE;
private ConnectionState mState = ConnectionState.OFFLINE;
private String mLastError;
private long mLastOnline = 0; //< timestamp of last successful full login (XEP-0198 does not count)
private long mLastOffline = 0; //< timestamp of the end of last successful login
private XMPPServiceCallback mServiceCallBack;
private Roster mRoster;
private RosterListener mRosterListener;
private PacketListener mPacketListener;
private PacketListener mPresenceListener;
private ConnectionListener mConnectionListener;
private final ContentResolver mContentResolver;
private AlarmManager mAlarmManager;
private PacketListener mPongListener;
private String mPingID;
private long mPingTimestamp;
private PendingIntent mPingAlarmPendIntent;
private PendingIntent mPongTimeoutAlarmPendIntent;
private static final String PING_ALARM = "org.yaxim.androidclient.PING_ALARM";
private static final String PONG_TIMEOUT_ALARM = "org.yaxim.androidclient.PONG_TIMEOUT_ALARM";
private Intent mPingAlarmIntent = new Intent(PING_ALARM);
private Intent mPongTimeoutAlarmIntent = new Intent(PONG_TIMEOUT_ALARM);
private Service mService;
private PongTimeoutAlarmReceiver mPongTimeoutAlarmReceiver = new PongTimeoutAlarmReceiver();
private BroadcastReceiver mPingAlarmReceiver = new PingAlarmReceiver();
private final HashSet<String> mucJIDs = new HashSet<String>(); //< all configured MUCs, joined or not
private Map<String, MultiUserChat> multiUserChats;
private long mucLastPing = 0;
private Map<String, Long> mucLastPong = new HashMap<String, Long>(); //< per-MUC timestamp of last incoming ping result
private Map<String, Presence> subscriptionRequests = new HashMap<String, Presence>();
public SmackableImp(YaximConfiguration config,
ContentResolver contentResolver,
Service service) {
this.mConfig = config;
this.mContentResolver = contentResolver;
this.mService = service;
this.mAlarmManager = (AlarmManager)mService.getSystemService(Context.ALARM_SERVICE);
mLastOnline = mLastOffline = System.currentTimeMillis();
}
// this code runs a DNS resolver, might be blocking
private synchronized void initXMPPConnection() {
// allow custom server / custom port to override SRV record
if (mConfig.customServer.length() > 0)
mXMPPConfig = new ConnectionConfiguration(mConfig.customServer,
mConfig.port, mConfig.server);
else
mXMPPConfig = new ConnectionConfiguration(mConfig.server); // use SRV
mXMPPConfig.setReconnectionAllowed(false);
mXMPPConfig.setSendPresence(false);
mXMPPConfig.setCompressionEnabled(false); // disable for now
mXMPPConfig.setDebuggerEnabled(mConfig.smackdebug);
if (mConfig.require_ssl)
this.mXMPPConfig.setSecurityMode(ConnectionConfiguration.SecurityMode.required);
// register MemorizingTrustManager for HTTPS
try {
SSLContext sc = SSLContext.getInstance("TLS");
MemorizingTrustManager mtm = YaximApplication.getApp(mService).mMTM;
sc.init(null, new X509TrustManager[] { mtm },
new java.security.SecureRandom());
this.mXMPPConfig.setCustomSSLContext(sc);
this.mXMPPConfig.setHostnameVerifier(mtm.wrapHostnameVerifier(
new org.apache.http.conn.ssl.StrictHostnameVerifier()));
} catch (java.security.GeneralSecurityException e) {
debugLog("initialize MemorizingTrustManager: " + e);
}
this.mXMPPConnection = new XmppStreamHandler.ExtXMPPConnection(mXMPPConfig);
mXMPPConnection.setParsingExceptionCallback(new ParsingExceptionCallback() {
@Override
public void handleUnparsablePacket(UnparsablePacket up) throws Exception {
Exception e = up.getParsingException();
// work around Smack throwing up on mod_mam_archive bug
if (e.getMessage().equals("variable cannot be null")) {
debugLog("Ignoring invalid disco#info caused by https://prosody.im/issues/issue/870");
e.printStackTrace();
return;
}
throw e;
}
});
this.mStreamHandler = new XmppStreamHandler(mXMPPConnection, mConfig.smackdebug);
mStreamHandler.addAckReceivedListener(new XmppStreamHandler.AckReceivedListener() {
public void ackReceived(long handled, long total) {
gotServerPong("" + handled);
}
});
mConfig.reconnect_required = false;
multiUserChats = new HashMap<String, MultiUserChat>();
initServiceDiscovery();
}
// blocking, run from a thread!
public boolean doConnect(boolean create_account) throws YaximXMPPException {
mRequestedState = ConnectionState.ONLINE;
updateConnectionState(ConnectionState.CONNECTING);
if (mXMPPConnection == null || mConfig.reconnect_required)
initXMPPConnection();
tryToConnect(create_account);
// actually, authenticated must be true now, or an exception must have
// been thrown.
if (isAuthenticated()) {
updateConnectionState(ConnectionState.LOADING);
registerMessageListener();
registerPresenceListener();
registerPongListener();
syncDbRooms();
sendOfflineMessages();
sendUserWatching();
// we need to "ping" the service to let it know we are actually
// connected, even when no roster entries will come in
updateConnectionState(ConnectionState.ONLINE);
} else throw new YaximXMPPException("SMACK connected, but authentication failed");
return true;
}
// BLOCKING, call on a new Thread!
private void updateConnectingThread(Thread new_thread) {
synchronized(mConnectingThreadMutex) {
if (mConnectingThread == null) {
mConnectingThread = new_thread;
} else try {
Log.d(TAG, "updateConnectingThread: old thread is still running, killing it.");
mConnectingThread.interrupt();
mConnectingThread.join(50);
} catch (InterruptedException e) {
Log.d(TAG, "updateConnectingThread: failed to join(): " + e);
} finally {
mConnectingThread = new_thread;
}
}
}
private void finishConnectingThread() {
synchronized(mConnectingThreadMutex) {
mConnectingThread = null;
}
}
/** Non-blocking, synchronized function to connect/disconnect XMPP.
* This code is called from outside and returns immediately. The actual work
* is done on a background thread, and notified via callback.
* @param new_state The state to transition into. Possible values:
* OFFLINE to properly close the connection
* ONLINE to connect
* DISCONNECTED when network goes down
* @param create_account When going online, try to register an account.
*/
@Override
public synchronized void requestConnectionState(ConnectionState new_state, final boolean create_account) {
Log.d(TAG, "requestConnState: " + mState + " -> " + new_state + (create_account ? " create_account!" : ""));
mRequestedState = new_state;
if (new_state == mState)
return;
switch (new_state) {
case ONLINE:
switch (mState) {
case RECONNECT_DELAYED:
// TODO: cancel timer
case RECONNECT_NETWORK:
case DISCONNECTED:
case OFFLINE:
// update state before starting thread to prevent race conditions
updateConnectionState(ConnectionState.CONNECTING);
// register ping (connection) timeout handler: 2*PACKET_TIMEOUT(30s) + 3s
registerPongTimeout(2*PACKET_TIMEOUT + 3000, "connection");
new Thread() {
@Override
public void run() {
updateConnectingThread(this);
try {
doConnect(create_account);
} catch (IllegalArgumentException e) {
// this might happen when DNS resolution in ConnectionConfiguration fails
onDisconnected(e);
} catch (IllegalStateException e) {//TODO: work around old smack
onDisconnected(e);
} catch (YaximXMPPException e) {
onDisconnected(e);
} finally {
mAlarmManager.cancel(mPongTimeoutAlarmPendIntent);
finishConnectingThread();
}
}
}.start();
break;
case CONNECTING:
case LOADING:
case DISCONNECTING:
// ignore all other cases
break;
}
break;
case DISCONNECTED:
// spawn thread to do disconnect
if (mState == ConnectionState.ONLINE) {
// update state before starting thread to prevent race conditions
updateConnectionState(ConnectionState.DISCONNECTING);
// register ping (connection) timeout handler: PACKET_TIMEOUT(30s)
registerPongTimeout(PACKET_TIMEOUT, "forced disconnect");
new Thread() {
public void run() {
updateConnectingThread(this);
mStreamHandler.quickShutdown();
onDisconnected("forced disconnect completed");
finishConnectingThread();
//updateConnectionState(ConnectionState.OFFLINE);
}
}.start();
}
break;
case OFFLINE:
switch (mState) {
case CONNECTING:
case LOADING:
case ONLINE:
// update state before starting thread to prevent race conditions
updateConnectionState(ConnectionState.DISCONNECTING);
// register ping (connection) timeout handler: PACKET_TIMEOUT(30s)
registerPongTimeout(PACKET_TIMEOUT, "manual disconnect");
// spawn thread to do disconnect
new Thread() {
public void run() {
updateConnectingThread(this);
mXMPPConnection.shutdown();
mStreamHandler.close();
mAlarmManager.cancel(mPongTimeoutAlarmPendIntent);
// we should reset XMPPConnection the next time
mConfig.reconnect_required = true;
finishConnectingThread();
// reconnect if it was requested in the meantime
if (mRequestedState == ConnectionState.ONLINE)
requestConnectionState(ConnectionState.ONLINE);
}
}.start();
break;
case DISCONNECTING:
break;
case DISCONNECTED:
case RECONNECT_DELAYED:
// TODO: clear timer
case RECONNECT_NETWORK:
updateConnectionState(ConnectionState.OFFLINE);
}
break;
case RECONNECT_NETWORK:
case RECONNECT_DELAYED:
switch (mState) {
case DISCONNECTED:
case RECONNECT_NETWORK:
case RECONNECT_DELAYED:
updateConnectionState(new_state);
break;
default:
throw new IllegalArgumentException("Can not go from " + mState + " to " + new_state);
}
}
}
@Override
public void requestConnectionState(ConnectionState new_state) {
requestConnectionState(new_state, false);
}
@Override
public ConnectionState getConnectionState() {
return mState;
}
@Override
public long getConnectionStateTimestamp() {
return (mState == ConnectionState.ONLINE) ? mLastOnline : mLastOffline;
}
// called at the end of a state transition
private synchronized void updateConnectionState(ConnectionState new_state) {
if (new_state == ConnectionState.ONLINE || new_state == ConnectionState.LOADING)
mLastError = null;
Log.d(TAG, "updateConnectionState: " + mState + " -> " + new_state + " (" + mLastError + ")");
if (new_state == mState)
return;
if (mState == ConnectionState.ONLINE)
mLastOffline = System.currentTimeMillis();
mState = new_state;
if (mServiceCallBack != null)
mServiceCallBack.connectionStateChanged();
}
private void initServiceDiscovery() {
// register connection features
ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(mXMPPConnection);
// init Entity Caps manager with storage in app's cache dir
try {
if (capsCacheDir == null) {
capsCacheDir = new File(mService.getCacheDir(), "entity-caps-cache");
capsCacheDir.mkdirs();
EntityCapsManager.setPersistentCache(new SimpleDirectoryPersistentCache(capsCacheDir));
}
} catch (java.io.IOException e) {
Log.e(TAG, "Could not init Entity Caps cache: " + e.getLocalizedMessage());
}
// reference PingManager, set ping flood protection to 10s
PingManager.getInstanceFor(mXMPPConnection).disablePingFloodProtection();
// set Version for replies
String app_name = mService.getString(R.string.app_name);
String build_revision = mService.getString(R.string.build_revision);
Version.Manager.getInstanceFor(mXMPPConnection).setVersion(
new Version(app_name, build_revision, "Android"));
// reference DeliveryReceiptManager, add listener
DeliveryReceiptManager dm = DeliveryReceiptManager.getInstanceFor(mXMPPConnection);
dm.addReceiptReceivedListener(new ReceiptReceivedListener() { // DOES NOT WORK IN CARBONS
public void onReceiptReceived(String fromJid, String toJid, String receiptId) {
Log.d(TAG, "got delivery receipt for " + receiptId);
changeMessageDeliveryStatus(receiptId, ChatConstants.DS_ACKED);
}});
}
public void addRosterItem(String user, String alias, String group, String token)
throws YaximXMPPException {
subscriptionRequests.remove(user);
mConfig.whitelistInvitationJID(user);
tryToAddRosterEntry(user, alias, group, token);
}
public void removeRosterItem(String user) throws YaximXMPPException {
debugLog("removeRosterItem(" + user + ")");
subscriptionRequests.remove(user);
tryToRemoveRosterEntry(user);
}
public void renameRosterItem(String user, String newName)
throws YaximXMPPException {
RosterEntry rosterEntry = mRoster.getEntry(user);
if (!(newName.length() > 0) || (rosterEntry == null)) {
throw new YaximXMPPException("JabberID to rename is invalid!");
}
rosterEntry.setName(newName);
}
public void addRosterGroup(String group) {
mRoster.createGroup(group);
}
public void renameRosterGroup(String group, String newGroup) {
RosterGroup groupToRename = mRoster.getGroup(group);
groupToRename.setName(newGroup);
}
public void moveRosterItemToGroup(String user, String group)
throws YaximXMPPException {
tryToMoveRosterEntryToGroup(user, group);
}
public void sendPresenceRequest(String user, String type) {
// HACK: remove the fake roster entry added by handleIncomingSubscribe()
subscriptionRequests.remove(user);
if ("unsubscribed".equals(type))
deleteRosterEntryFromDB(user);
Presence response = new Presence(Presence.Type.valueOf(type));
response.setTo(user);
mXMPPConnection.sendPacket(response);
}
@Override
public String changePassword(String newPassword) {
try {
new AccountManager(mXMPPConnection).changePassword(newPassword);
return "OK"; //HACK: hard coded string to differentiate from failure modes
} catch (XMPPException e) {
if (e.getXMPPError() != null)
return e.getXMPPError().toString();
else
return e.getLocalizedMessage();
}
}
private void onDisconnected(String reason) {
unregisterPongListener();
mLastError = reason;
updateConnectionState(ConnectionState.DISCONNECTED);
}
private void onDisconnected(Throwable reason) {
Log.e(TAG, "onDisconnected: " + reason);
reason.printStackTrace();
// iterate through to the deepest exception
while (reason.getCause() != null && !(reason.getCause().getClass().getSimpleName().equals("GaiException")))
reason = reason.getCause();
onDisconnected(reason.getLocalizedMessage());
}
private void tryToConnect(boolean create_account) throws YaximXMPPException {
try {
if (mXMPPConnection.isConnected()) {
try {
mStreamHandler.quickShutdown(); // blocking shutdown prior to re-connection
} catch (Exception e) {
debugLog("conn.shutdown() failed: " + e);
}
}
registerRosterListener();
boolean need_bind = !mStreamHandler.isResumePossible();
if (mConnectionListener != null)
mXMPPConnection.removeConnectionListener(mConnectionListener);
mConnectionListener = new ConnectionListener() {
public void connectionClosedOnError(Exception e) {
// XXX: this is the only callback we get from errors, so
// we need to check for non-resumability and work around
// here:
if (!mStreamHandler.isResumePossible()) {
for (MultiUserChat muc : multiUserChats.values())
muc.cleanup();
multiUserChats.clear();
mucLastPong.clear();
mucLastPing = 0;
}
onDisconnected(e);
}
public void connectionClosed() {
// TODO: fix reconnect when we got kicked by the server or SM failed!
//onDisconnected(null);
for (MultiUserChat muc : multiUserChats.values())
muc.cleanup();
multiUserChats.clear();
mucLastPong.clear();
mucLastPing = 0;
updateConnectionState(ConnectionState.OFFLINE);
}
public void reconnectingIn(int seconds) { }
public void reconnectionFailed(Exception e) { }
public void reconnectionSuccessful() { }
};
mXMPPConnection.addConnectionListener(mConnectionListener);
mXMPPConnection.connect(need_bind);
// SMACK auto-logins if we were authenticated before
if (!mXMPPConnection.isAuthenticated()) {
if (create_account) {
Log.d(TAG, "creating new server account...");
AccountManager am = new AccountManager(mXMPPConnection);
am.createAccount(mConfig.userName, mConfig.password);
}
mXMPPConnection.login(mConfig.userName, mConfig.password,
mConfig.ressource);
}
Log.d(TAG, "SM: can resume = " + mStreamHandler.isResumePossible() + " needbind=" + need_bind);
if (need_bind) {
mStreamHandler.notifyInitialLogin();
cleanupMUCs(true);
setStatusFromConfig();
discoverMUCDomainAsync();
mLastOnline = System.currentTimeMillis();
}
} catch (Exception e) {
// actually we just care for IllegalState or NullPointer or XMPPEx.
throw new YaximXMPPException("tryToConnect failed", e);
}
}
private void tryToMoveRosterEntryToGroup(String userName, String groupName)
throws YaximXMPPException {
RosterGroup rosterGroup = getRosterGroup(groupName);
RosterEntry rosterEntry = mRoster.getEntry(userName);
removeRosterEntryFromGroups(rosterEntry);
if (groupName.length() == 0)
return;
else {
try {
rosterGroup.addEntry(rosterEntry);
} catch (XMPPException e) {
throw new YaximXMPPException("tryToMoveRosterEntryToGroup", e);
}
}
}
private RosterGroup getRosterGroup(String groupName) {
RosterGroup rosterGroup = mRoster.getGroup(groupName);
// create group if unknown
if ((groupName.length() > 0) && rosterGroup == null) {
rosterGroup = mRoster.createGroup(groupName);
}
return rosterGroup;
}
private void removeRosterEntryFromGroups(RosterEntry rosterEntry)
throws YaximXMPPException {
Collection<RosterGroup> oldGroups = rosterEntry.getGroups();
for (RosterGroup group : oldGroups) {
tryToRemoveUserFromGroup(group, rosterEntry);
}
}
private void tryToRemoveUserFromGroup(RosterGroup group,
RosterEntry rosterEntry) throws YaximXMPPException {
try {
group.removeEntry(rosterEntry);
} catch (XMPPException e) {
throw new YaximXMPPException("tryToRemoveUserFromGroup", e);
}
}
private void tryToRemoveRosterEntry(String user) throws YaximXMPPException {
try {
RosterEntry rosterEntry = mRoster.getEntry(user);
if (rosterEntry != null) {
// first, unsubscribe the user
Presence unsub = new Presence(Presence.Type.unsubscribed);
unsub.setTo(rosterEntry.getUser());
mXMPPConnection.sendPacket(unsub);
// then, remove from roster
mRoster.removeEntry(rosterEntry);
}
} catch (XMPPException e) {
throw new YaximXMPPException("tryToRemoveRosterEntry", e);
}
}
private void tryToAddRosterEntry(String user, String alias, String group, String token)
throws YaximXMPPException {
try {
// send a presence subscription request with token (must be before roster action!)
if (token != null && token.length() > 0) {
Presence preauth = new Presence(Presence.Type.subscribe);
preauth.setTo(user);
preauth.addExtension(new PreAuth(token));
mXMPPConnection.sendPacket(preauth);
}
// add to roster, triggers another sub request by Smack (sigh)
mRoster.createEntry(user, alias, new String[] { group });
// send a pre-approval
Presence pre_approval = new Presence(Presence.Type.subscribed);
pre_approval.setTo(user);
mXMPPConnection.sendPacket(pre_approval);
mConfig.whitelistInvitationJID(user);
} catch (XMPPException e) {
throw new YaximXMPPException("tryToAddRosterEntry", e);
}
}
private void removeOldRosterEntries() {
Log.d(TAG, "removeOldRosterEntries()");
Collection<RosterEntry> rosterEntries = mRoster.getEntries();
StringBuilder exclusion = new StringBuilder(RosterConstants.JID + " NOT IN (");
boolean first = true;
for (RosterEntry rosterEntry : rosterEntries) {
if (first)
first = false;
else
exclusion.append(",");
exclusion.append("'").append(rosterEntry.getUser()).append("'");
}
exclusion.append(") AND "+RosterConstants.GROUP+" NOT IN ('" + RosterProvider.RosterConstants.MUCS + "');");
int count = mContentResolver.delete(RosterProvider.CONTENT_URI, exclusion.toString(), null);
Log.d(TAG, "deleted " + count + " old roster entries");
}
// HACK: add an incoming subscription request as a fake roster entry
private void handleIncomingSubscribe(Presence request) {
// perform Pre-Authenticated Roster Subscription, fallback to manual
try {
String jid = request.getFrom();
PreAuth preauth = (PreAuth)request.getExtension(PreAuth.ELEMENT, PreAuth.NAMESPACE);
String jid_or_token = jid;
if (preauth != null) {
jid_or_token = preauth.getToken();
Log.d(TAG, "PARS: found token " + jid_or_token);
}
if (mConfig.redeemInvitationCode(jid_or_token)) {
Log.d(TAG, "PARS: approving request from " + jid);
if (mRoster.getEntry(request.getFrom()) != null) {
// already in roster, only send approval
Presence response = new Presence(Presence.Type.subscribed);
response.setTo(jid);
mXMPPConnection.sendPacket(response);
} else {
tryToAddRosterEntry(jid, null, "", null);
}
return;
}
} catch (YaximXMPPException e) {
Log.d(TAG, "PARS: failed to send response: " + e);
}
subscriptionRequests.put(request.getFrom(), request);
final ContentValues values = new ContentValues();
values.put(RosterConstants.JID, request.getFrom());
values.put(RosterConstants.STATUS_MODE, getStatusInt(request));
values.put(RosterConstants.STATUS_MESSAGE, request.getStatus());
if (!mRoster.contains(request.getFrom())) {
// reset alias and group for new entries
values.put(RosterConstants.ALIAS, request.getFrom());
values.put(RosterConstants.GROUP, "");
};
upsertRoster(values, request.getFrom());
}
public void setStatusFromConfig() {
// TODO: only call this when carbons changed, not on every presence change
CarbonManager.getInstanceFor(mXMPPConnection).sendCarbonsEnabled(mConfig.messageCarbons);
Presence presence = new Presence(Presence.Type.available);
Mode mode = Mode.valueOf(mConfig.getPresenceMode().toString());
presence.setMode(mode);
presence.setStatus(mConfig.statusMessage);
presence.setPriority(mConfig.priority);
mXMPPConnection.sendPacket(presence);
mConfig.presence_required = false;
}
public void sendOfflineMessages() {
Cursor cursor = mContentResolver.query(ChatProvider.CONTENT_URI,
SEND_OFFLINE_PROJECTION, SEND_OFFLINE_SELECTION,
null, null);
final int _ID_COL = cursor.getColumnIndexOrThrow(ChatConstants._ID);
final int JID_COL = cursor.getColumnIndexOrThrow(ChatConstants.JID);
final int MSG_COL = cursor.getColumnIndexOrThrow(ChatConstants.MESSAGE);
final int TS_COL = cursor.getColumnIndexOrThrow(ChatConstants.DATE);
final int PACKETID_COL = cursor.getColumnIndexOrThrow(ChatConstants.PACKET_ID);
ContentValues mark_sent = new ContentValues();
mark_sent.put(ChatConstants.DELIVERY_STATUS, ChatConstants.DS_SENT_OR_READ);
while (cursor.moveToNext()) {
int _id = cursor.getInt(_ID_COL);
String toJID = cursor.getString(JID_COL);
String message = cursor.getString(MSG_COL);
String packetID = cursor.getString(PACKETID_COL);
long ts = cursor.getLong(TS_COL);
Log.d(TAG, "sendOfflineMessages: " + toJID + " > " + message);
final Message newMessage = new Message(toJID, Message.Type.chat);
newMessage.setBody(message);
DelayInformation delay = new DelayInformation(new Date(ts));
newMessage.addExtension(delay);
newMessage.addExtension(new DelayInfo(delay));
if (mucJIDs.contains(toJID))
newMessage.setType(Message.Type.groupchat);
else
newMessage.addExtension(new DeliveryReceiptRequest());
if ((packetID != null) && (packetID.length() > 0)) {
newMessage.setPacketID(packetID);
} else {
packetID = newMessage.getPacketID();
}
mark_sent.put(ChatConstants.PACKET_ID, packetID);
Uri rowuri = Uri.parse("content://" + ChatProvider.AUTHORITY
+ "/" + ChatProvider.TABLE_NAME + "/" + _id);
mContentResolver.update(rowuri, mark_sent,
null, null);
mXMPPConnection.sendPacket(newMessage); // must be after marking delivered, otherwise it may override the SendFailListener
}
cursor.close();
}
public static void sendOfflineMessage(ContentResolver cr, String toJID, String message) {
ContentValues values = new ContentValues();
values.put(ChatConstants.DIRECTION, ChatConstants.OUTGOING);
values.put(ChatConstants.JID, toJID);
values.put(ChatConstants.MESSAGE, message);
values.put(ChatConstants.DELIVERY_STATUS, ChatConstants.DS_NEW);
values.put(ChatConstants.DATE, System.currentTimeMillis());
values.put(ChatConstants.PACKET_ID, Packet.nextID());
cr.insert(ChatProvider.CONTENT_URI, values);
}
public void sendReceiptIfRequested(Packet packet) {
DeliveryReceiptRequest drr = (DeliveryReceiptRequest)packet.getExtension(
DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE);
if (drr != null) {
Message ack = new Message(packet.getFrom(), Message.Type.normal);
ack.addExtension(new DeliveryReceipt(packet.getPacketID()));
mXMPPConnection.sendPacket(ack);
}
}
public void sendMessage(String toJID, String message) {
final Message newMessage = new Message(toJID, Message.Type.chat);
newMessage.setBody(message);
newMessage.addExtension(new DeliveryReceiptRequest());
if (isAuthenticated()) {
if(mucJIDs.contains(toJID)) {
sendMucMessage(toJID, message);
} else {
addChatMessageToDB(ChatConstants.OUTGOING, toJID, message, ChatConstants.DS_SENT_OR_READ,
System.currentTimeMillis(), newMessage.getPacketID());
mXMPPConnection.sendPacket(newMessage);
}
} else {
// send offline -> store to DB
addChatMessageToDB(ChatConstants.OUTGOING, toJID, message, ChatConstants.DS_NEW,
System.currentTimeMillis(), newMessage.getPacketID());
}
}
public boolean isAuthenticated() {
if (mXMPPConnection != null) {
return (mXMPPConnection.isConnected() && mXMPPConnection
.isAuthenticated());
}
return false;
}
public void registerCallback(XMPPServiceCallback callBack) {
this.mServiceCallBack = callBack;
mService.registerReceiver(mPingAlarmReceiver, new IntentFilter(PING_ALARM));
mService.registerReceiver(mPongTimeoutAlarmReceiver, new IntentFilter(PONG_TIMEOUT_ALARM));
}
public void unRegisterCallback() {
debugLog("unRegisterCallback()");
// remove callbacks _before_ tossing old connection
try {
mXMPPConnection.getRoster().removeRosterListener(mRosterListener);
mXMPPConnection.removePacketListener(mPacketListener);
mXMPPConnection.removePacketListener(mPresenceListener);
mXMPPConnection.removePacketListener(mPongListener);
unregisterPongListener();
} catch (Exception e) {
// ignore it!
e.printStackTrace();
}
requestConnectionState(ConnectionState.OFFLINE);
setStatusOffline();
mService.unregisterReceiver(mPingAlarmReceiver);
mService.unregisterReceiver(mPongTimeoutAlarmReceiver);
// multiUserChats.clear(); // TODO: right place
this.mServiceCallBack = null;
}
public String getNameForJID(String jid) {
if (jid.contains("/")) { // MUC-PM
String[] jid_parts = jid.split("/", 2);
return String.format("%s (%s)", jid_parts[1],
ChatRoomHelper.getRoomName(mService, jid_parts[0]));
}
RosterEntry re = mRoster.getEntry(jid);
if (null != re && null != re.getName() && re.getName().length() > 0) {
return re.getName();
} else if (mucJIDs.contains(jid)) {
return ChatRoomHelper.getRoomName(mService, jid);
} else {
return jid;
}
}
public long getRowIdForMessage(String jid, String resource, int direction, String packet_id) {
// query the DB for the RowID, return -1 if packet_id does not match
Cursor c = mContentResolver.query(ChatProvider.CONTENT_URI, new String[] { ChatConstants._ID, ChatConstants.PACKET_ID },
"jid = ? AND resource = ? AND from_me = ?",
new String[] { jid, resource, "" + direction }, "_id DESC");
long result = -1;
if (c.moveToFirst() && c.getString(1).equals(packet_id))
result = c.getLong(0);
c.close();
return result;
}
private void setStatusOffline() {
ContentValues values = new ContentValues();
values.put(RosterConstants.STATUS_MODE, StatusMode.offline.ordinal());
mContentResolver.update(RosterProvider.CONTENT_URI, values, null, null);
}
private void registerRosterListener() {
// flush roster on connecting.
mRoster = mXMPPConnection.getRoster();
mRoster.setSubscriptionMode(Roster.SubscriptionMode.manual);
if (mRosterListener != null)
mRoster.removeRosterListener(mRosterListener);
mRosterListener = new RosterListener() {
private boolean first_roster = true;
public void entriesAdded(Collection<String> entries) {
debugLog("entriesAdded(" + entries + ")");
ContentValues[] cvs = new ContentValues[entries.size()];
int i = 0;
for (String entry : entries) {
RosterEntry rosterEntry = mRoster.getEntry(entry);
cvs[i++] = getContentValuesForRosterEntry(rosterEntry);
}
mContentResolver.bulkInsert(RosterProvider.CONTENT_URI, cvs);
// when getting the roster in the beginning, remove remains of old one
if (first_roster) {
removeOldRosterEntries();
first_roster = false;
}
debugLog("entriesAdded() done");
}
public void entriesDeleted(Collection<String> entries) {
debugLog("entriesDeleted(" + entries + ")");
for (String entry : entries) {
deleteRosterEntryFromDB(entry);
}
}
public void entriesUpdated(Collection<String> entries) {
debugLog("entriesUpdated(" + entries + ")");
for (String entry : entries) {
RosterEntry rosterEntry = mRoster.getEntry(entry);
updateRosterEntryInDB(rosterEntry);
}
}
public void presenceChanged(Presence presence) {
debugLog("presenceChanged(" + presence.getFrom() + "): " + presence);
String jabberID = getBareJID(presence.getFrom());
RosterEntry rosterEntry = mRoster.getEntry(jabberID);
if (rosterEntry != null)
upsertRoster(getContentValuesForRosterEntry(rosterEntry, presence),
rosterEntry.getUser());
}
};
mRoster.addRosterListener(mRosterListener);
}
private String getBareJID(String from) {
String[] res = from.split("/", 2);
return res[0].toLowerCase();
}
private String[] getJabberID(String from) {
if(from.contains("/")) {
String[] res = from.split("/", 2);
return new String[] { res[0], res[1] };
} else {
return new String[] {from, ""};
}
}
public boolean changeMessageDeliveryStatus(String packetID, int new_status) {
ContentValues cv = new ContentValues();
cv.put(ChatConstants.DELIVERY_STATUS, new_status);
Uri rowuri = Uri.parse("content://" + ChatProvider.AUTHORITY + "/"
+ ChatProvider.TABLE_NAME);
return mContentResolver.update(rowuri, cv,
ChatConstants.PACKET_ID + " = ? AND " +
ChatConstants.DELIVERY_STATUS + " != " + ChatConstants.DS_ACKED + " AND " +
ChatConstants.DIRECTION + " = " + ChatConstants.OUTGOING,
new String[] { packetID }) > 0;
}
protected boolean is_user_watching = false;
public void setUserWatching(boolean user_watching) {
if (is_user_watching == user_watching)
return;
is_user_watching = user_watching;
if (mXMPPConnection != null && mXMPPConnection.isAuthenticated())
sendUserWatching();
}
protected void sendUserWatching() {
IQ toggle_google_queue = new IQ() {
public String getChildElementXML() {
// enable g:q = start queueing packets = do it when the user is gone
return "<query xmlns='google:queue'><" + (is_user_watching ? "disable" : "enable") + "/></query>";
}
};
toggle_google_queue.setType(IQ.Type.SET);
mXMPPConnection.sendPacket(toggle_google_queue);
}
/** Check the server connection, reconnect if needed.
*
* This function will try to ping the server if we are connected, and try
* to reestablish a connection otherwise.
*/
public void sendServerPing() {
if (mXMPPConnection == null || !mXMPPConnection.isAuthenticated()) {
debugLog("Ping: requested, but not connected to server.");
requestConnectionState(ConnectionState.ONLINE, false);
return;
}
if (mPingID != null) {
debugLog("Ping: requested, but still waiting for " + mPingID);
return; // a ping is still on its way
}
if (mStreamHandler.isSmEnabled()) {
debugLog("Ping: sending SM request");
mPingID = "" + mStreamHandler.requestAck();
} else {
Ping ping = new Ping();
ping.setType(Type.GET);
ping.setTo(mConfig.server);
mPingID = ping.getPacketID();
debugLog("Ping: sending ping " + mPingID);
mXMPPConnection.sendPacket(ping);
}
// register ping timeout handler: PACKET_TIMEOUT(30s) + 3s
registerPongTimeout(PACKET_TIMEOUT + 3000, mPingID);
}
private void gotServerPong(String pongID) {
long latency = System.currentTimeMillis() - mPingTimestamp;
if (pongID != null && pongID.equals(mPingID))
Log.i(TAG, String.format("Ping: server latency %1.3fs",
latency/1000.));
else
Log.i(TAG, String.format("Ping: server latency %1.3fs (estimated)",
latency/1000.));
mPingID = null;
mAlarmManager.cancel(mPongTimeoutAlarmPendIntent);
}
/** Register a "pong" timeout on the connection. */
private void registerPongTimeout(long wait_time, String id) {
mPingID = id;
mPingTimestamp = System.currentTimeMillis();
debugLog(String.format("Ping: registering timeout for %s: %1.3fs", id, wait_time/1000.));
mAlarmManager.set(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + wait_time,
mPongTimeoutAlarmPendIntent);
}
/**
* BroadcastReceiver to trigger reconnect on pong timeout.
*/
private class PongTimeoutAlarmReceiver extends BroadcastReceiver {
public void onReceive(Context ctx, Intent i) {
debugLog("Ping: timeout for " + mPingID);
onDisconnected(mService.getString(R.string.conn_ping_timeout));
}
}
/**
* BroadcastReceiver to trigger sending pings to the server
*/
private class PingAlarmReceiver extends BroadcastReceiver {
public void onReceive(Context ctx, Intent i) {
try {
sendServerPing();
// ping all MUCs. if no ping was received since last attempt, /cycle
Iterator<MultiUserChat> muc_it = multiUserChats.values().iterator();
long ts = System.currentTimeMillis();
ContentValues cvR = new ContentValues();
cvR.put(RosterProvider.RosterConstants.STATUS_MESSAGE, mService.getString(R.string.conn_ping_timeout));
cvR.put(RosterProvider.RosterConstants.STATUS_MODE, StatusMode.offline.ordinal());
cvR.put(RosterProvider.RosterConstants.GROUP, RosterProvider.RosterConstants.MUCS);
while (muc_it.hasNext()) {
MultiUserChat muc = muc_it.next();
if (!muc.isJoined())
continue;
Long lastPong = mucLastPong.get(muc.getRoom());
if (mucLastPing > 0 && (lastPong == null || lastPong < mucLastPing)) {
debugLog("Ping timeout from " + muc.getRoom());
muc.leave();
upsertRoster(cvR, muc.getRoom());
} else {
Ping ping = new Ping();
ping.setType(Type.GET);
String jid = muc.getRoom() + "/" + muc.getNickname();
ping.setTo(jid);
mPingID = ping.getPacketID();
debugLog("Ping: sending ping to " + jid);
mXMPPConnection.sendPacket(ping);
}
}
syncDbRooms();
mucLastPing = ts;
} catch (NullPointerException npe) {
/* ignore disconnect race condition */
} catch (IllegalStateException ise) {
/* ignore disconnect race condition */
}
}
}
/**
* Registers a smack packet listener for IQ packets, intended to recognize "pongs" with
* a packet id matching the last "ping" sent to the server.
*
* Also sets up the AlarmManager Timer plus necessary intents.
*/
private void registerPongListener() {
// reset ping expectation on new connection
mPingID = null;
if (mPongListener != null)
mXMPPConnection.removePacketListener(mPongListener);
mPongListener = new PacketListener() {
@Override
public void processPacket(Packet packet) {
if (packet == null) return;
if (packet instanceof IQ && packet.getFrom() != null) {
IQ ping = (IQ)packet;
String from_bare = getBareJID(ping.getFrom());
// check for ping error or RESULT
if (ping.getType() == Type.RESULT && mucJIDs.contains(from_bare)) {
Log.d(TAG, "Ping: got response from MUC " + from_bare);
mucLastPong.put(from_bare, System.currentTimeMillis());
}
}
if (mPingID != null && mPingID.equals(packet.getPacketID()))
gotServerPong(packet.getPacketID());
}
};
mXMPPConnection.addPacketListener(mPongListener, new PacketTypeFilter(IQ.class));
mPingAlarmPendIntent = PendingIntent.getBroadcast(mService.getApplicationContext(), 0, mPingAlarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
mPongTimeoutAlarmPendIntent = PendingIntent.getBroadcast(mService.getApplicationContext(), 0, mPongTimeoutAlarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
mAlarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + AlarmManager.INTERVAL_FIFTEEN_MINUTES, AlarmManager.INTERVAL_FIFTEEN_MINUTES, mPingAlarmPendIntent);
}
private void unregisterPongListener() {
mAlarmManager.cancel(mPingAlarmPendIntent);
mAlarmManager.cancel(mPongTimeoutAlarmPendIntent);
}
private void registerMessageListener() {
// do not register multiple packet listeners
if (mPacketListener != null)
mXMPPConnection.removePacketListener(mPacketListener);
PacketTypeFilter filter = new PacketTypeFilter(Message.class);
mPacketListener = new PacketListener() {
public void processPacket(Packet packet) {
try {
if (packet instanceof Message) {
Message msg = (Message) packet;
String[] fromJID = getJabberID(msg.getFrom());
int direction = ChatConstants.INCOMING;
Carbon cc = CarbonManager.getCarbon(msg);
if (cc != null && !msg.getFrom().equalsIgnoreCase(mConfig.jabberID)) {
Log.w(TAG, "Received illegal carbon from " + msg.getFrom() + ": " + cc.toXML());
cc = null;
}
// extract timestamp
long ts;
DelayInfo timestamp = (DelayInfo)msg.getExtension("delay", "urn:xmpp:delay");
if (timestamp == null)
timestamp = (DelayInfo)msg.getExtension("x", "jabber:x:delay");
if (cc != null) // Carbon timestamp overrides packet timestamp
timestamp = cc.getForwarded().getDelayInfo();
if (timestamp != null)
ts = timestamp.getStamp().getTime();
else
ts = System.currentTimeMillis();
// try to extract a carbon
if (cc != null) {
Log.d(TAG, "carbon: " + cc.toXML());
msg = (Message)cc.getForwarded().getForwardedPacket();
// outgoing carbon: fromJID is actually chat peer's JID
if (cc.getDirection() == Carbon.Direction.sent) {
fromJID = getJabberID(msg.getTo());
direction = ChatConstants.OUTGOING;
} else {
fromJID = getJabberID(msg.getFrom());
// hook off carbonated delivery receipts
DeliveryReceipt dr = (DeliveryReceipt)msg.getExtension(
DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE);
if (dr != null) {
Log.d(TAG, "got CC'ed delivery receipt for " + dr.getId());
changeMessageDeliveryStatus(dr.getId(), ChatConstants.DS_ACKED);
}
}
// ignore carbon copies of OTR messages sent by broken clients
if (msg.getBody() != null && msg.getBody().startsWith("?OTR")) {
Log.i(TAG, "Ignoring OTR carbon from " + msg.getFrom() + " to " + msg.getTo());
return;
}
}
// check for jabber MUC invitation
if(direction == ChatConstants.INCOMING && handleMucInvitation(msg)) {
sendReceiptIfRequested(packet);
return;
}
String chatMessage = msg.getBody();
// display error inline
if (msg.getType() == Message.Type.error) {
if (changeMessageDeliveryStatus(msg.getPacketID(), ChatConstants.DS_FAILED))
mServiceCallBack.notifyMessage(fromJID, msg.getError().toString(), (cc != null), Message.Type.error);
else if (mucJIDs.contains(msg.getFrom())) {
handleKickedFromMUC(msg.getFrom(), false, null,
msg.getError().toString());
}
return; // we do not want to add errors as "incoming messages"
}
// ignore empty messages
if (chatMessage == null) {
if (msg.getSubject() != null && msg.getType() == Message.Type.groupchat
&& mucJIDs.contains(fromJID[0])) {
// this is a MUC subject, update our DB
ContentValues cvR = new ContentValues();
cvR.put(RosterProvider.RosterConstants.STATUS_MESSAGE, msg.getSubject());
cvR.put(RosterProvider.RosterConstants.STATUS_MODE, StatusMode.available.ordinal());
Log.d(TAG, "MUC subject for " + fromJID[0] + " set to: " + msg.getSubject());
upsertRoster(cvR, fromJID[0]);
return;
}
Log.d(TAG, "empty message.");
return;
}
// obtain Last Message Correction, if present
Replace replace = (Replace)msg.getExtension(Replace.NAMESPACE);
String replace_id = (replace != null) ? replace.getId() : null;
// carbons are old. all others are new
int is_new = (cc == null) ? ChatConstants.DS_NEW : ChatConstants.DS_SENT_OR_READ;
if (msg.getType() == Message.Type.error)
is_new = ChatConstants.DS_FAILED;
boolean is_muc = (msg.getType() == Message.Type.groupchat);
boolean is_from_me = (direction == ChatConstants.OUTGOING) ||
(is_muc && fromJID[1].equals(getMyMucNick(fromJID[0])));
// handle MUC-PMs: messages from a nick from a known MUC or with
// an <x> element
MUCUser muc_x = (MUCUser)msg.getExtension("x", "http://jabber.org/protocol/muc#user");
boolean is_muc_pm = !is_muc && !TextUtils.isEmpty(fromJID[1]) &&
(muc_x != null || mucJIDs.contains(fromJID[0]));
// TODO: ignoring 'received' MUC-PM carbons, until XSF sorts out shit:
// - if yaxim is in the MUC, it will receive a non-carbonated copy of
// incoming messages, but not of outgoing ones
// - if yaxim isn't in the MUC, it can't respond anyway
if (is_muc_pm && !is_from_me && cc != null)
return;
if (is_muc_pm) {
// store MUC-PMs under the participant's full JID, not bare
//is_from_me = fromJID[1].equals(getMyMucNick(fromJID[0]));
fromJID[0] = fromJID[0] + "/" + fromJID[1];
fromJID[1] = null;
Log.d(TAG, "MUC-PM: " + fromJID[0] + " d=" + direction + " fromme=" + is_from_me);
}
// Carbons and MUC history are 'silent' by default
boolean is_silent = (cc != null) || (is_muc && timestamp != null);
if (!is_muc || checkAddMucMessage(msg, msg.getPacketID(), fromJID, timestamp)) {
addChatMessageToDB(direction, fromJID, chatMessage, is_new, ts, msg.getPacketID(), replace_id);
// only notify on private messages or when MUC notification requested
boolean need_notify = !is_muc || mConfig.needMucNotification(getMyMucNick(fromJID[0]), chatMessage);
// outgoing carbon -> clear notification by signalling 'null' message
if (is_from_me) {
mServiceCallBack.notifyMessage(fromJID, null, true, msg.getType());
// TODO: MUC PMs
ChatHelper.markAsRead(mService, fromJID[0]);
} else if (direction == ChatConstants.INCOMING && need_notify)
mServiceCallBack.notifyMessage(fromJID, chatMessage, is_silent, msg.getType());
}
sendReceiptIfRequested(packet);
}
} catch (Exception e) {
// SMACK silently discards exceptions dropped from processPacket :(
Log.e(TAG, "failed to process packet:");
e.printStackTrace();
}
}
};
mXMPPConnection.addPacketListener(mPacketListener, filter);
}
private boolean checkAddMucMessage(Message msg, String packet_id, String[] fromJid, DelayInfo timestamp) {
String muc = fromJid[0];
String nick = fromJid[1];
// HACK: remove last outgoing message instead of upserting
if (nick.equals(getMyMucNick(muc)))
mContentResolver.delete(ChatProvider.CONTENT_URI,
"jid = ? AND from_me = 1 AND (pid = ? OR message = ?) AND " +
"_id >= (SELECT MIN(_id) FROM chats WHERE jid = ? ORDER BY _id DESC LIMIT 50)",
new String[] { muc, packet_id, msg.getBody(), muc });
// messages with no timestamp are always new
if (timestamp == null)
return true;
long ts = timestamp.getStamp().getTime();
final String[] projection = new String[] {
ChatConstants._ID, ChatConstants.MESSAGE,
ChatConstants.JID, ChatConstants.RESOURCE,
ChatConstants.PACKET_ID
};
if (packet_id == null) packet_id = "";
final String selection = "resource = ? AND (pid = ? OR date = ? OR message = ?) AND _id >= (SELECT MIN(_id) FROM chats WHERE jid = ? ORDER BY _id DESC LIMIT 50)";
final String[] selectionArgs = new String[] { nick, packet_id, ""+ts, msg.getBody(), muc };
try {
Cursor cursor = mContentResolver.query(ChatProvider.CONTENT_URI, projection, selection, selectionArgs, null);
Log.d(TAG, "message from " + nick + " matched " + cursor.getCount() + " items.");
boolean result = (cursor.getCount() == 0);
cursor.close();
return result;
} catch (Exception e) { e.printStackTrace(); } // just return true...
return true;
}
private void handleKickedFromMUC(String room, boolean banned, String actor, String reason) {
mucLastPong.remove(room);
ContentValues cvR = new ContentValues();
String message;
if (actor != null && actor.length() > 0)
message = mService.getString(banned ? R.string.muc_banned_by : R.string.muc_kicked_by,
actor, reason);
else
message = mService.getString(banned ? R.string.muc_banned : R.string.muc_kicked,
reason);
cvR.put(RosterProvider.RosterConstants.STATUS_MESSAGE, message);
cvR.put(RosterProvider.RosterConstants.STATUS_MODE, StatusMode.offline.ordinal());
upsertRoster(cvR, room);
}
@Override
public String getMyMucNick(String jid) {
MultiUserChat muc = multiUserChats.get(jid);
if (muc != null && muc.getNickname() != null)
return muc.getNickname();
if (mucJIDs.contains(jid)) {
ChatRoomHelper.RoomInfo ri = ChatRoomHelper.getRoomInfo(mService, jid);
if (ri != null)
return ri.nickname;
}
return null;
}
private void registerPresenceListener() {
// do not register multiple packet listeners
if (mPresenceListener != null)
mXMPPConnection.removePacketListener(mPresenceListener);
mPresenceListener = new PacketListener() {
public void processPacket(Packet packet) {
try {
Presence p = (Presence) packet;
switch (p.getType()) {
case subscribe:
handleIncomingSubscribe(p);
break;
case subscribed:
case unsubscribe:
case unsubscribed:
subscriptionRequests.remove(p.getFrom());
break;
}
} catch (Exception e) {
// SMACK silently discards exceptions dropped from processPacket :(
Log.e(TAG, "failed to process presence:");
e.printStackTrace();
}
}
};
mXMPPConnection.addPacketListener(mPresenceListener, new PacketTypeFilter(Presence.class));
}
private void addChatMessageToDB(int direction, String[] tJID,
String message, int delivery_status, long ts, String packetID, String replaceID) {
ContentValues values = new ContentValues();
values.put(ChatConstants.DIRECTION, direction);
values.put(ChatConstants.JID, tJID[0]);
values.put(ChatConstants.RESOURCE, tJID[1]);
values.put(ChatConstants.MESSAGE, message);
values.put(ChatConstants.DELIVERY_STATUS, delivery_status);
values.put(ChatConstants.DATE, ts);
values.put(ChatConstants.PACKET_ID, packetID);
if (replaceID != null) {
// obtain row id for last message with that full JID, or -1
long _id = getRowIdForMessage(tJID[0], tJID[1], direction, replaceID);
Log.d(TAG, "Replacing last message from " + tJID[0] + "/" + tJID[1] + ": " + replaceID + " -> " + packetID);
Uri row = Uri.withAppendedPath(ChatProvider.CONTENT_URI, "" + _id);
if (_id >= 0 && mContentResolver.update(row, values, null, null) == 1)
return;
}
mContentResolver.insert(ChatProvider.CONTENT_URI, values);
}
private void addChatMessageToDB(int direction, String JID,
String message, int delivery_status, long ts, String packetID) {
String[] tJID = {JID, ""};
addChatMessageToDB(direction, tJID, message, delivery_status, ts, packetID, null);
}
private ContentValues getContentValuesForRosterEntry(final RosterEntry entry) {
Presence presence = mRoster.getPresence(entry.getUser());
return getContentValuesForRosterEntry(entry, presence);
}
private ContentValues getContentValuesForRosterEntry(final RosterEntry entry, Presence presence) {
final ContentValues values = new ContentValues();
values.put(RosterConstants.JID, entry.getUser());
values.put(RosterConstants.ALIAS, getName(entry));
// handle subscription requests and errors with higher priority
Presence sub = subscriptionRequests.get(entry.getUser());
if (presence.getType() == Presence.Type.error) {
String error = presence.getError().getMessage();
if (error == null || error.length() == 0)
error = presence.getError().toString();
values.put(RosterConstants.STATUS_MESSAGE, error);
} else if (sub != null) {
presence = sub;
values.put(RosterConstants.STATUS_MESSAGE, presence.getStatus());
} else switch (entry.getType()) {
case to:
case both:
// override received presence from roster, using highest-prio entry
presence = mRoster.getPresence(entry.getUser());
values.put(RosterConstants.STATUS_MESSAGE, presence.getStatus());
break;
case from:
values.put(RosterConstants.STATUS_MESSAGE, mService.getString(R.string.subscription_status_from));
presence = null;
break;
case none:
values.put(RosterConstants.STATUS_MESSAGE, "");
presence = null;
}
values.put(RosterConstants.STATUS_MODE, getStatusInt(presence));
values.put(RosterConstants.GROUP, getGroup(entry.getGroups()));
return values;
}
private void deleteRosterEntryFromDB(final String jabberID) {
int count = mContentResolver.delete(RosterProvider.CONTENT_URI,
RosterConstants.JID + " = ?", new String[] { jabberID });
debugLog("deleteRosterEntryFromDB: Deleted " + count + " entries");
}
private void updateRosterEntryInDB(final RosterEntry entry) {
upsertRoster(getContentValuesForRosterEntry(entry), entry.getUser());
}
private void upsertRoster(final ContentValues values, String jid) {
if (mContentResolver.update(RosterProvider.CONTENT_URI, values,
RosterConstants.JID + " = ?", new String[] { jid }) == 0) {
mContentResolver.insert(RosterProvider.CONTENT_URI, values);
}
}
private String getGroup(Collection<RosterGroup> groups) {
for (RosterGroup group : groups) {
return group.getName();
}
return "";
}
private String getName(RosterEntry rosterEntry) {
String name = rosterEntry.getName();
if (name != null && name.length() > 0) {
return name;
}
return rosterEntry.getUser();
}
private StatusMode getStatus(Presence presence) {
if (presence == null)
return StatusMode.unknown;
if (presence.getType() == Presence.Type.subscribe)
return StatusMode.subscribe;
if (presence.getType() == Presence.Type.available) {
if (presence.getMode() != null) {
return StatusMode.valueOf(presence.getMode().name());
}
return StatusMode.available;
}
return StatusMode.offline;
}
private int getStatusInt(final Presence presence) {
return getStatus(presence).ordinal();
}
private void debugLog(String data) {
if (LogConstants.LOG_DEBUG) {
Log.d(TAG, data);
}
}
@Override
public String getLastError() {
return mLastError;
}
private void discoverMUCDomain() {
Log.d(TAG, "discoverMUCDomain started");
try {
Collection<String> mucDomains = MultiUserChat.getServiceNames(mXMPPConnection);
if (mucDomains.size() >= 1)
mConfig.mucDomain = mucDomains.iterator().next();
Log.d(TAG, "discoverMUCDomain finished: " + mucDomains.size() + " entries, using " + mConfig.mucDomain);
} catch (Exception e) {
Log.d(TAG, "discoverMUCDomain failed: " + e.getMessage());
}
}
private void discoverMUCDomainAsync() {
new Thread() {
public void run() {
discoverMUCDomain();
}
}.start();
}
private synchronized void cleanupMUCs(boolean set_offline) {
// get a fresh MUC list
Cursor cursor = mContentResolver.query(RosterProvider.MUCS_URI,
new String[] { RosterProvider.RosterConstants.JID },
null, null, null);
mucJIDs.clear();
while(cursor.moveToNext()) {
mucJIDs.add(cursor.getString(0));
}
cursor.close();
// delete removed MUCs
StringBuilder exclusion = new StringBuilder(RosterProvider.RosterConstants.GROUP + " = ? AND "
+ RosterConstants.JID + " NOT IN ('");
exclusion.append(TextUtils.join("', '", mucJIDs));
exclusion.append("');");
mContentResolver.delete(RosterProvider.CONTENT_URI,
exclusion.toString(),
new String[] { RosterProvider.RosterConstants.MUCS });
if (set_offline) {
// update all other MUCs as offline
ContentValues values = new ContentValues();
values.put(RosterConstants.STATUS_MODE, StatusMode.offline.ordinal());
mContentResolver.update(RosterProvider.CONTENT_URI, values, RosterProvider.RosterConstants.GROUP + " = ?",
new String[] { RosterProvider.RosterConstants.MUCS });
}
}
public synchronized void syncDbRooms() {
if (!isAuthenticated()) {
debugLog("syncDbRooms: aborting, not yet authenticated");
}
java.util.Set<String> joinedRooms = multiUserChats.keySet();
Cursor cursor = mContentResolver.query(RosterProvider.MUCS_URI,
new String[] {RosterProvider.RosterConstants._ID,
RosterProvider.RosterConstants.JID,
RosterProvider.RosterConstants.PASSWORD,
RosterProvider.RosterConstants.NICKNAME},
null, null, null);
final int ID = cursor.getColumnIndexOrThrow(RosterProvider.RosterConstants._ID);
final int JID_ID = cursor.getColumnIndexOrThrow(RosterProvider.RosterConstants.JID);
final int PASSWORD_ID = cursor.getColumnIndexOrThrow(RosterProvider.RosterConstants.PASSWORD);
final int NICKNAME_ID = cursor.getColumnIndexOrThrow(RosterProvider.RosterConstants.NICKNAME);
mucJIDs.clear();
while(cursor.moveToNext()) {
int id = cursor.getInt(ID);
String jid = cursor.getString(JID_ID);
String password = cursor.getString(PASSWORD_ID);
String nickname = cursor.getString(NICKNAME_ID);
mucJIDs.add(jid);
//debugLog("Found MUC Room: "+jid+" with nick "+nickname+" and pw "+password);
if(!joinedRooms.contains(jid) || !multiUserChats.get(jid).isJoined()) {
debugLog("room " + jid + " isn't joined yet, i wanna join...");
joinRoomAsync(jid, nickname, password); // TODO: make historyLen configurable
} else {
MultiUserChat muc = multiUserChats.get(jid);
if (!muc.getNickname().equals(nickname)) {
debugLog("room " + jid + ": changing nickname to " + nickname);
try {
muc.changeNickname(nickname);
} catch (XMPPException e) {
Log.e(TAG, "Changing nickname failed.");
e.printStackTrace();
}
}
}
//debugLog("found data in contentprovider: "+jid+" "+password+" "+nickname);
}
cursor.close();
for(String room : new HashSet<String>(joinedRooms)) {
if(!mucJIDs.contains(room)) {
quitRoom(room);
}
}
cleanupMUCs(false);
}
protected boolean handleMucInvitation(Message msg) {
String room;
String inviter = null;
String reason = null;
String password = null;
MUCUser mucuser = (MUCUser)msg.getExtension("x", "http://jabber.org/protocol/muc#user");
GroupChatInvitation direct = (GroupChatInvitation)msg.getExtension(GroupChatInvitation.ELEMENT_NAME, GroupChatInvitation.NAMESPACE);
if (mucuser != null && mucuser.getInvite() != null) {
// first try official XEP-0045 mediated invitation
MUCUser.Invite invite = mucuser.getInvite();
room = msg.getFrom();
inviter = invite.getFrom();
reason = invite.getReason();
password = mucuser.getPassword();
} else if (direct != null) {
// fall back to XEP-0249 direct invitation
room = direct.getRoomAddress();
inviter = msg.getFrom();
// TODO: get reason from direct invitation, not supported in smack3
} else return false; // not a MUC invitation
if (mucJIDs.contains(room)) {
Log.i(TAG, "Ignoring invitation to known MUC " + room);
return true;
}
Log.d(TAG, "MUC invitation from " + inviter + " to " + room);
asyncProcessMucInvitation(room, inviter, reason, password);
return true;
}
protected void asyncProcessMucInvitation(final String room, final String inviter,
final String reason, final String password) {
new Thread() {
public void run() {
processMucInvitation(room, inviter, reason, password);
}
}.start();
}
protected void processMucInvitation(final String room, final String inviter,
final String reason, final String password) {
String roomname = room;
String inviter_name = null;
if (getBareJID(inviter).equalsIgnoreCase(room)) {
// from == participant JID, display as "user (MUC)"
inviter_name = getNameForJID(inviter);
} else {
// from == user bare or full JID
inviter_name = getNameForJID(getBareJID(inviter));
}
String description = null;
String inv_from = mService.getString(R.string.muc_invitation_from,
inviter_name);
// query room for info
try {
Log.d(TAG, "Requesting disco#info from " + room);
RoomInfo ri = MultiUserChat.getRoomInfo(mXMPPConnection, room);
String rn = ri.getRoomName();
if (rn != null && rn.length() > 0)
roomname = String.format("%s (%s)", rn, roomname);
description = ri.getSubject();
if (!TextUtils.isEmpty(description))
description = ri.getDescription();
description = mService.getString(R.string.muc_invitation_occupants,
description, ri.getOccupantsCount());
Log.d(TAG, "MUC name after disco: " + roomname);
} catch (XMPPException e) {
// ignore a failed room info request
Log.d(TAG, "MUC room IQ failed: " + room);
e.printStackTrace();
}
mServiceCallBack.mucInvitationReceived(
roomname,
room,
password,
inv_from,
description);
}
private Map<String,Runnable> ongoingMucJoins = new java.util.concurrent.ConcurrentHashMap<String, Runnable>();
private synchronized void joinRoomAsync(final String room, final String nickname, final String password) {
if (ongoingMucJoins.containsKey(room))
return;
Thread joiner = new Thread() {
@Override
public void run() {
Log.d(TAG, "async joining " + room);
boolean result = joinRoom(room, nickname, password);
Log.d(TAG, "async joining " + room + " done: " + result);
ongoingMucJoins.remove(room);
}
};
ongoingMucJoins.put(room, joiner);
joiner.start();
}
private boolean joinRoom(final String room, String nickname, String password) {
// work around smack3 bug: can't rejoin with "used" MultiUserChat instance; need to manually
// flush old MUC instance and create a new.
MultiUserChat muc = multiUserChats.get(room);
if (muc != null)
muc.cleanup();
muc = new MultiUserChat(mXMPPConnection, room);
Log.d(TAG, "created new MUC instance: " + room + " " + muc);
muc.addUserStatusListener(new org.jivesoftware.smackx.muc.DefaultUserStatusListener() {
@Override
public void kicked(String actor, String reason) {
debugLog("Kicked from " + room + " by " + actor + ": " + reason);
handleKickedFromMUC(room, false, actor, reason);
}
@Override
public void banned(String actor, String reason) {
debugLog("Banned from " + room + " by " + actor + ": " + reason);
handleKickedFromMUC(room, true, actor, reason);
}
});
DiscussionHistory history = new DiscussionHistory();
final String[] projection = new String[] {
ChatConstants._ID, ChatConstants.DATE
};
Cursor cursor = mContentResolver.query(ChatProvider.CONTENT_URI, projection,
ChatConstants.JID + " = ? AND " +
ChatConstants.DELIVERY_STATUS + " = " +
ChatConstants.DS_SENT_OR_READ, new String[] { room }, "_id DESC LIMIT 1");
if(cursor.getCount()>0) {
cursor.moveToFirst();
Date lastDate = new Date(cursor.getLong(1));
Log.d(TAG, "Getting room history for " + room + " starting at " + lastDate);
history.setSince(lastDate);
} else Log.d(TAG, "No last message for " + room);
cursor.close();
ContentValues cvR = new ContentValues();
cvR.put(RosterProvider.RosterConstants.JID, room);
cvR.put(RosterProvider.RosterConstants.ALIAS, room);
cvR.put(RosterProvider.RosterConstants.STATUS_MESSAGE, mService.getString(R.string.muc_synchronizing));
cvR.put(RosterProvider.RosterConstants.STATUS_MODE, StatusMode.dnd.ordinal());
cvR.put(RosterProvider.RosterConstants.GROUP, RosterProvider.RosterConstants.MUCS);
upsertRoster(cvR, room);
cvR.clear();
cvR.put(RosterProvider.RosterConstants.JID, room);
try {
Presence force_resync = new Presence(Presence.Type.unavailable);
force_resync.setTo(room + "/" + nickname);
mXMPPConnection.sendPacket(force_resync);
muc.join(nickname, password, history, 10*PACKET_TIMEOUT);
} catch (Exception e) {
Log.e(TAG, "Could not join MUC-room "+room);
e.printStackTrace();
cvR.put(RosterProvider.RosterConstants.STATUS_MESSAGE, mService.getString(R.string.conn_error, e.getLocalizedMessage()));
cvR.put(RosterProvider.RosterConstants.STATUS_MODE, StatusMode.offline.ordinal());
upsertRoster(cvR, room);
muc.cleanup();
return false;
}
if(muc.isJoined()) {
synchronized(this) {
multiUserChats.put(room, muc);
}
String roomname = room.split("@")[0];
try {
RoomInfo ri = MultiUserChat.getRoomInfo(mXMPPConnection, room);
String rn = ri.getRoomName();
if (rn != null && rn.length() > 0)
roomname = rn;
Log.d(TAG, "MUC name after disco: " + roomname);
} catch (XMPPException e) {
// ignore a failed room info request
Log.d(TAG, "MUC room IQ failed: " + room);
e.printStackTrace();
}
// delay requesting subject until room info IQ returned/failed
String subject = muc.getSubject();
cvR.put(RosterProvider.RosterConstants.ALIAS, roomname);
cvR.put(RosterProvider.RosterConstants.STATUS_MESSAGE, subject);
cvR.put(RosterProvider.RosterConstants.STATUS_MODE, StatusMode.available.ordinal());
Log.d(TAG, "upserting MUC as online: " + roomname);
upsertRoster(cvR, room);
return true;
}
muc.cleanup();
return false;
}
@Override
public void sendMucMessage(String room, String message) {
Message newMessage = new Message(room, Message.Type.groupchat);
newMessage.setBody(message);
addChatMessageToDB(ChatConstants.OUTGOING, room, message, ChatConstants.DS_NEW,
System.currentTimeMillis(), newMessage.getPacketID());
mXMPPConnection.sendPacket(newMessage);
}
private void quitRoom(String room) {
Log.d(TAG, "Leaving MUC " + room);
MultiUserChat muc = multiUserChats.get(room);
muc.leave();
multiUserChats.remove(room);
mucLastPong.remove(room);
mContentResolver.delete(RosterProvider.CONTENT_URI, "jid = ?", new String[] {room});
}
@Override
public boolean inviteToRoom(String contactJid, String roomJid) {
MultiUserChat muc = multiUserChats.get(roomJid);
if(contactJid.contains("/")) {
contactJid = contactJid.split("/")[0];
}
Log.d(TAG, "invitng contact: "+contactJid+" to room: "+muc);
muc.invite(contactJid, "User "+contactJid+" has invited you to a chat!");
return false;
}
@Override
public List<ParcelablePresence> getUserList(String jid) {
MultiUserChat muc = multiUserChats.get(jid);
if (muc == null) {
return null;
}
Log.d(TAG, "MUC instance: " + jid + " " + muc);
Iterator<String> occIter = muc.getOccupants();
ArrayList<ParcelablePresence> tmpList = new ArrayList<ParcelablePresence>();
while(occIter.hasNext())
tmpList.add(new ParcelablePresence(muc.getOccupantPresence(occIter.next())));
Collections.sort(tmpList, new Comparator<ParcelablePresence>() {
@Override
public int compare(ParcelablePresence lhs, ParcelablePresence rhs) {
return java.text.Collator.getInstance().compare(lhs.resource, rhs.resource);
}
});
Log.d(TAG, "getUserList(" + jid + "): " + tmpList.size());
return tmpList;
}
}