handle reconnections - requires asmack mods
[gitian:android-secim.git] / src / org / gitian / android / im / plugin / xmpp / XmppConnection.java
1 package org.gitian.android.im.plugin.xmpp;
2
3 import java.util.ArrayList;
4 import java.util.Collection;
5 import java.util.Date;
6 import java.util.HashMap;
7 import java.util.Iterator;
8
9 import org.gitian.android.im.engine.Address;
10 import org.gitian.android.im.engine.ChatGroupManager;
11 import org.gitian.android.im.engine.ChatSession;
12 import org.gitian.android.im.engine.ChatSessionManager;
13 import org.gitian.android.im.engine.Contact;
14 import org.gitian.android.im.engine.ContactList;
15 import org.gitian.android.im.engine.ContactListListener;
16 import org.gitian.android.im.engine.ContactListManager;
17 import org.gitian.android.im.engine.ImConnection;
18 import org.gitian.android.im.engine.ImErrorInfo;
19 import org.gitian.android.im.engine.ImException;
20 import org.gitian.android.im.engine.LoginInfo;
21 import org.gitian.android.im.engine.Message;
22 import org.gitian.android.im.engine.Presence;
23 import org.jivesoftware.smack.ConnectionConfiguration;
24 import org.jivesoftware.smack.ConnectionListener;
25 import org.jivesoftware.smack.PacketCollector;
26 import org.jivesoftware.smack.PacketListener;
27 import org.jivesoftware.smack.ReconnectionManager;
28 import org.jivesoftware.smack.Roster;
29 import org.jivesoftware.smack.RosterEntry;
30 import org.jivesoftware.smack.RosterGroup;
31 import org.jivesoftware.smack.RosterListener;
32 import org.jivesoftware.smack.SmackConfiguration;
33 import org.jivesoftware.smack.XMPPConnection;
34 import org.jivesoftware.smack.XMPPException;
35 import org.jivesoftware.smack.filter.AndFilter;
36 import org.jivesoftware.smack.filter.MessageTypeFilter;
37 import org.jivesoftware.smack.filter.PacketFilter;
38 import org.jivesoftware.smack.filter.PacketIDFilter;
39 import org.jivesoftware.smack.filter.PacketTypeFilter;
40 import org.jivesoftware.smack.packet.IQ;
41 import org.jivesoftware.smack.packet.Packet;
42 import org.jivesoftware.smack.packet.Presence.Mode;
43 import org.jivesoftware.smack.packet.Presence.Type;
44
45 import android.os.Parcel;
46 import android.util.Log;
47
48 public class XmppConnection extends ImConnection {
49
50         protected static final String TAG = "XmppConnection";
51         private static final long PING_TIMEOUT = 5000;
52         private XmppContactList mContactListManager;
53         private Contact mUser;
54         private MyXMPPConnection mConnection;
55         private XmppChatSessionManager mSessionManager;
56
57         public XmppConnection() {
58                 Log.d(TAG, "created");
59                 ReconnectionManager.activate();
60                 SmackConfiguration.setKeepAliveInterval(-1);
61         }
62         
63         // TODO !tests
64         // TODO !battery tests
65         // TODO !OTR
66         // TODO !beta
67         // FIXME NPEs in contact handling
68         // FIXME leaking binder threads
69         // FIXME IllegalStateException in ReconnectionManager
70         
71         @Override
72         protected void doUpdateUserPresenceAsync(Presence presence) {
73                 String statusText = presence.getStatusText();
74         Type type = Type.available;
75         Mode mode = Mode.available;
76         int priority = 20;
77         if (presence.getStatus() == Presence.AWAY) {
78                 priority = 10;
79                 mode = Mode.away;
80         }
81         else if (presence.getStatus() == Presence.DO_NOT_DISTURB) {
82                 priority = 1;
83                 mode = Mode.dnd;
84         }
85         else if (presence.getStatus() == Presence.OFFLINE) {
86                 priority = 0;
87                 type = Type.unavailable;
88                 statusText = "Offline";
89         }
90                 org.jivesoftware.smack.packet.Presence packet = 
91                 new org.jivesoftware.smack.packet.Presence(type, statusText, priority, mode);
92         mConnection.sendPacket(packet);
93
94         }
95
96         @Override
97         public int getCapability() {
98                 // TODO chat groups
99                 return 0;
100         }
101
102         @Override
103         public ChatGroupManager getChatGroupManager() {
104                 // TODO chat groups
105                 return null;
106         }
107
108         @Override
109         public ChatSessionManager getChatSessionManager() {
110                 mSessionManager = new XmppChatSessionManager();
111                 return mSessionManager;
112         }
113
114         @Override
115         public ContactListManager getContactListManager() {
116                 mContactListManager = new XmppContactList();
117                 return mContactListManager;
118         }
119
120         @Override
121         public Contact getLoginUser() {
122                 return mUser;
123         }
124
125         @Override
126         public HashMap<String, String> getSessionContext() {
127                 return null;
128         }
129
130         @Override
131         public int[] getSupportedPresenceStatus() {
132                 return new int[] {
133                                 Presence.AVAILABLE,
134                                 Presence.AWAY,
135                                 Presence.IDLE,
136                                 Presence.OFFLINE,
137                                 Presence.DO_NOT_DISTURB,
138                 };
139         }
140
141         @Override
142         public void loginAsync(LoginInfo loginInfo) {
143                 Log.i(TAG, "loggin in " + loginInfo.getUserName());
144                 setState(LOGGING_IN, null);
145                 mUserPresence = new Presence(Presence.AVAILABLE, "available", null, null, Presence.CLIENT_TYPE_DEFAULT);
146                 String username = loginInfo.getUserName();
147                 String []comps = username.split("@");
148                 if (comps.length != 2)
149                         throw new RuntimeException("username should be user@host");
150                 try {
151                         initConnection(comps[1], comps[0], loginInfo.getPassword(), "Android");
152                 } catch (XMPPException e) {
153                         forced_disconnect(new ImErrorInfo(ImErrorInfo.CANT_CONNECT_TO_SERVER, e.getMessage()));
154                         return;
155                 }
156                 mUser = new Contact(new XmppAddress(comps[0], username), username);
157                 setState(LOGGED_IN, null);
158                 Log.i(TAG, "logged in");
159         }
160
161         private void initConnection(String serverHost, String login, String password, String resource) throws XMPPException {
162         ConnectionConfiguration config = new ConnectionConfiguration(serverHost);
163                 mConnection = new MyXMPPConnection(config);
164                 startPingTask();
165         mConnection.connect();
166         mConnection.addPacketListener(new PacketListener() {
167                         
168                         @Override
169                         public void processPacket(Packet packet) {
170                                 org.jivesoftware.smack.packet.Message message = (org.jivesoftware.smack.packet.Message) packet;
171                                 Message rec = new Message(message.getBody());
172                                 String address = parseAddressBase(message.getFrom());
173                                 ChatSession session = findOrCreateSession(address);
174                                 rec.setFrom(session.getParticipant().getAddress());
175                                 rec.setDateTime(new Date());
176                                 session.onReceiveMessage(rec);
177                         }
178                 }, new MessageTypeFilter(org.jivesoftware.smack.packet.Message.Type.chat));
179         mConnection.addPacketListener(new PacketListener() {
180                         
181                         @Override
182                         public void processPacket(Packet packet) {
183                                 org.jivesoftware.smack.packet.Presence presence = (org.jivesoftware.smack.packet.Presence)packet;
184                                 if (presence.getType() == Type.subscribe) {
185                                         String address = parseAddressBase(presence.getFrom());
186                                         Log.i(TAG, "sub request from " + address);
187                                         Contact contact = findOrCreateContact(address);
188                                         mContactListManager.getSubscriptionRequestListener().onSubScriptionRequest(contact);
189                                 }
190                         }
191                 }, new PacketTypeFilter(org.jivesoftware.smack.packet.Presence.class));
192         mConnection.addConnectionListener(new ConnectionListener() {
193                         @Override
194                         public void reconnectionSuccessful() {
195                                 Log.i(TAG, "reconnection success");
196                         }
197                         
198                         @Override
199                         public void reconnectionFailed(Exception e) {
200                                 // TODO reconnection failed
201                                 Log.i(TAG, "reconnection failed");
202                                 forced_disconnect(new ImErrorInfo(ImErrorInfo.NETWORK_ERROR, e.getMessage()));
203                         }
204                         
205                         @Override
206                         public void reconnectingIn(int seconds) {
207                                 // TODO reconnecting
208                                 Log.i(TAG, "reconnecting in " + seconds);
209                                 if (seconds == 0)
210                                         startPingTask();
211                         }
212                         
213                         @Override
214                         public void connectionClosedOnError(Exception e) {
215                                 Log.i(TAG, "closed on error", e);
216                                 pingThread = null;
217                         }
218                         
219                         @Override
220                         public void connectionClosed() {
221                                 Log.i(TAG, "connection closed");
222                                 forced_disconnect(null);
223                         }
224                 });
225         mConnection.login(login, password, resource);
226         org.jivesoftware.smack.packet.Presence presence = 
227                 new org.jivesoftware.smack.packet.Presence(org.jivesoftware.smack.packet.Presence.Type.available);
228         mConnection.sendPacket(presence);
229         }
230         
231         void forced_disconnect(ImErrorInfo info) {
232                 setState(DISCONNECTED, info);
233                 try {
234                         if (mConnection!= null) {
235                                 XMPPConnection conn = mConnection;
236                                 mConnection = null;
237                                 conn.disconnect();
238                         }
239                 }
240                 catch (Exception e) {
241                         // Ignore
242                 }
243                 Log.i(TAG, "connection cleaned up");
244         }
245
246         protected static String parseAddressBase(String from) {
247                 return from.replaceFirst("/.*", "");
248         }
249
250         protected static String parseAddressUser(String from) {
251                 return from.replaceFirst("@.*", "");
252         }
253
254         @Override
255         public void logoutAsync() {
256                 Log.i(TAG, "logout");
257                 setState(LOGGING_OUT, null);
258                 XMPPConnection conn = mConnection;
259                 mConnection = null;
260                 conn.disconnect();
261         }
262
263         @Override
264         public void reestablishSessionAsync(HashMap<String, String> sessionContext) {
265
266         }
267
268         @Override
269         public void suspend() {
270
271         }
272
273         private ChatSession findOrCreateSession(String address) {
274                 ChatSession session = mSessionManager.findSession(address);
275                 if (session == null) {
276                         Contact contact = findOrCreateContact(address);
277                         session = mSessionManager.createChatSession(contact);
278                 }
279                 return session;
280         }
281
282         private Contact findOrCreateContact(String address) {
283                 Contact contact = mContactListManager.getContact(address);
284                 if (contact == null) {
285                         contact = makeContact(address);
286                 }
287                 return contact;
288         }
289
290         private static String makeNameFromAddress(String address) {
291                 return address;
292         }
293
294         private static Contact makeContact(String address) {
295                 Contact contact = new Contact(new XmppAddress(address), address);
296                 return contact;
297         }
298
299         private final class XmppChatSessionManager extends ChatSessionManager {
300                 @Override
301                 protected void sendMessageAsync(ChatSession session, Message message) {
302                         org.jivesoftware.smack.packet.Message msg =
303                                 new org.jivesoftware.smack.packet.Message(
304                                                 message.getTo().getFullName(),
305                                                 org.jivesoftware.smack.packet.Message.Type.chat
306                                                 );
307                         msg.setBody(message.getBody());
308                         mConnection.sendPacket(msg);
309                 }
310                 
311                 ChatSession findSession(String address) {
312                         for (Iterator<ChatSession> iter = mSessions.iterator(); iter.hasNext();) {
313                                 ChatSession session = iter.next();
314                                 if (session.getParticipant().getAddress().getFullName().equals(address))
315                                         return session;
316                         }
317                         return null;
318                 }
319         }
320
321         private final class XmppContactList extends ContactListManager {
322                 @Override
323                 protected void setListNameAsync(String name, ContactList list) {
324                         Log.d(TAG, "set list name");
325                         mConnection.getRoster().getGroup(list.getName()).setName(name);
326                         notifyContactListNameUpdated(list, name);
327                 }
328
329                 @Override
330                 public String normalizeAddress(String address) {
331                         return address;
332                 }
333
334                 @Override
335                 public void loadContactListsAsync() {
336                         Log.d(TAG, "load contact lists");
337                         Roster roster = mConnection.getRoster();
338                         roster.setSubscriptionMode(Roster.SubscriptionMode.manual);
339                         listenToRoster(roster);
340                         boolean haveGroup = false;
341                         for (Iterator<RosterGroup> giter = roster.getGroups().iterator(); giter.hasNext();) {
342                                 haveGroup = true;
343                                 RosterGroup group = giter.next();
344                                 Collection<Contact> contacts = new ArrayList<Contact>();
345                                 Contact[] contacts_array = new Contact[group.getEntryCount()];
346                                 for (Iterator<RosterEntry> iter = group.getEntries().iterator(); iter.hasNext();) {
347                                         RosterEntry entry = iter.next();
348                                         XmppAddress addr = new XmppAddress(entry.getName(), parseAddressBase(entry.getUser()));
349                                         Contact contact = new Contact(addr, entry.getName());
350                                         contacts.add(contact);
351                                 }
352                                 ContactList cl = new ContactList(mUser.getAddress(), group.getName(), true, contacts, this);
353                                 mContactLists.add(cl);
354                                 if (mDefaultContactList == null)
355                                         mDefaultContactList = cl;
356                                 notifyContactListLoaded(cl);
357                                 notifyContactsPresenceUpdated(contacts.toArray(contacts_array));
358                         }
359                         if (!haveGroup) {
360                                 roster.createGroup("Friends");
361                                 ContactList cl = new ContactList(mUser.getAddress(), "Friends" , true, new ArrayList<Contact>(), this);
362                                 mDefaultContactList = cl;
363                                 notifyContactListLoaded(cl);
364                         }
365                         notifyContactListsLoaded();
366                 }
367
368                 private void listenToRoster(final Roster roster) {
369                         roster.addRosterListener(new RosterListener() {
370                                 
371                                 @Override
372                                 public void presenceChanged(org.jivesoftware.smack.packet.Presence presence) {
373                                         String user = parseAddressBase(presence.getFrom());
374                                         Contact contact = mContactListManager.getContact(user);
375                                         if (contact == null)
376                                                 return;
377                                         Contact []contacts = new Contact[] { contact };
378                                         // Get it from the roster - it handles priorities, etc.
379                                         presence = roster.getPresence(user);
380                                         int type = Presence.AVAILABLE;
381                                         Mode rmode = presence.getMode();
382                                         Type rtype = presence.getType();
383                                         if (rmode == Mode.away || rmode == Mode.xa)
384                                                 type = Presence.AWAY;
385                                         if (rmode == Mode.dnd)
386                                                 type = Presence.DO_NOT_DISTURB;
387                                         if (rtype == Type.unavailable)
388                                                 type = Presence.OFFLINE;
389                                         contact.setPresence(new Presence(type, presence.getStatus(), null, null, Presence.CLIENT_TYPE_DEFAULT));
390                                         notifyContactsPresenceUpdated(contacts);
391                                 }
392                                 
393                                 @Override
394                                 public void entriesUpdated(Collection<String> addresses) {
395                                         // TODO update contact list entries from remote
396                                         Log.d(TAG, "roster entries updated");
397                                 }
398                                 
399                                 @Override
400                                 public void entriesDeleted(Collection<String> addresses) {
401                                         // TODO delete contacts from remote
402                                         Log.d(TAG, "roster entries deleted");
403                                 }
404                                 
405                                 @Override
406                                 public void entriesAdded(Collection<String> addresses) {
407                                         // TODO add contacts from remote
408                                         Log.d(TAG, "roster entries added");
409                                 }
410                         });
411                 }
412
413                 @Override
414                 protected ImConnection getConnection() {
415                         return XmppConnection.this;
416                 }
417
418                 @Override
419                 protected void doRemoveContactFromListAsync(Contact contact,
420                                 ContactList list) {
421                         Roster roster = mConnection.getRoster();
422                         String address = contact.getAddress().getFullName();
423                         try {
424                                 RosterGroup group = roster.getGroup(list.getName());
425                                 if (group == null) {
426                                         Log.e(TAG, "could not find group " + list.getName() + " in roster");
427                                         return;
428                                 }
429                                 RosterEntry entry = roster.getEntry(address);
430                                 if (entry == null) {
431                                         Log.e(TAG, "could not find entry " + address + " in group " + list.getName());
432                                         return;
433                                 }
434                                 group.removeEntry(entry);
435                         } catch (XMPPException e) {
436                                 Log.e(TAG, "remove entry failed", e);
437                                 throw new RuntimeException(e);
438                         }
439             org.jivesoftware.smack.packet.Presence response =
440                 new org.jivesoftware.smack.packet.Presence(org.jivesoftware.smack.packet.Presence.Type.unsubscribed);
441             response.setTo(address);
442             mConnection.sendPacket(response);
443                         notifyContactListUpdated(list, ContactListListener.LIST_CONTACT_REMOVED, contact);
444                 }
445
446                 @Override
447                 protected void doDeleteContactListAsync(ContactList list) {
448                         // TODO delete contact list
449                         Log.i(TAG, "delete contact list " + list.getName());
450                 }
451
452                 @Override
453                 protected void doCreateContactListAsync(String name,
454                                 Collection<Contact> contacts, boolean isDefault) {
455                         // TODO create contact list
456                         Log.i(TAG, "create contact list " + name + " default " + isDefault);
457                 }
458
459                 @Override
460                 protected void doBlockContactAsync(String address, boolean block) {
461                         // TODO block contact
462                         
463                 }
464
465                 @Override
466                 protected void doAddContactToListAsync(String address, ContactList list)
467                                 throws ImException {
468                         Log.i(TAG, "add contact to " + list.getName());
469                         org.jivesoftware.smack.packet.Presence response =
470                                 new org.jivesoftware.smack.packet.Presence(org.jivesoftware.smack.packet.Presence.Type.subscribed);
471                         response.setTo(address);
472                         mConnection.sendPacket(response);
473
474                         Roster roster = mConnection.getRoster();
475                         String[] groups = new String[] { list.getName() };
476                         try {
477                                 roster.createEntry(address, makeNameFromAddress(address), groups);
478                         } catch (XMPPException e) {
479                                 throw new RuntimeException(e);
480                         }
481                         
482                         Contact contact = makeContact(address);
483                         notifyContactListUpdated(list, ContactListListener.LIST_CONTACT_ADDED, contact);
484                 }
485
486                 @Override
487                 public void declineSubscriptionRequest(String contact) {
488                         Log.d(TAG, "decline subscription");
489             org.jivesoftware.smack.packet.Presence response =
490                 new org.jivesoftware.smack.packet.Presence(org.jivesoftware.smack.packet.Presence.Type.unsubscribed);
491             response.setTo(contact);
492             mConnection.sendPacket(response);
493             mContactListManager.getSubscriptionRequestListener().onSubscriptionDeclined(contact);
494                 }
495
496                 @Override
497                 public void approveSubscriptionRequest(String contact) {
498                         Log.d(TAG, "approve subscription");
499             try {
500                 // FIXME maybe need to check if already in another contact list
501                                 mContactListManager.doAddContactToListAsync(contact, getDefaultContactList());
502                         } catch (ImException e) {
503                                 Log.e(TAG, "failed to add " + contact + " to default list");
504                         }
505             mContactListManager.getSubscriptionRequestListener().onSubscriptionApproved(contact);
506                 }
507
508                 @Override
509                 public Contact createTemporaryContact(String address) {
510                         Log.d(TAG, "create temporary " + address);
511                         return makeContact(address);
512                 }
513         }
514
515         public static class XmppAddress extends Address {
516                 
517                 private String address;
518                 private String name;
519
520                 public XmppAddress() {
521                 }
522                 
523                 public XmppAddress(String name, String address) {
524                         this.name = name;
525                         this.address = address;
526                 }
527                 
528                 public XmppAddress(String address) {
529                         this.name = makeNameFromAddress(address);
530                         this.address = address;
531                 }
532
533                 @Override
534                 public String getFullName() {
535                         return address;
536                 }
537
538                 @Override
539                 public String getScreenName() {
540                         return name;
541                 }
542
543                 @Override
544                 public void readFromParcel(Parcel source) {
545                         name = source.readString();
546                         address = source.readString();
547                 }
548
549                 @Override
550                 public void writeToParcel(Parcel dest) {
551                         dest.writeString(name);
552                         dest.writeString(address);
553                 }
554                 
555         }
556
557         Thread pingThread;
558
559         void startPingTask() {
560                 // Schedule a ping task to run.
561                 PingTask task = new PingTask(PING_TIMEOUT);
562                 pingThread = new Thread(task);
563                 task.setThread(pingThread);
564                 pingThread.setDaemon(true);
565                 pingThread.setName("XmppConnection Pinger");
566                 pingThread.start();
567         }
568
569         public boolean sendPing() {
570                 IQ req = new IQ() {
571                         public String getChildElementXML() {
572                                 return "<ping xmlns='urn:xmpp:ping'/>";
573                         }
574                 };
575                 req.setType(IQ.Type.GET);
576         PacketFilter filter = new AndFilter(new PacketIDFilter(req.getPacketID()),
577                 new PacketTypeFilter(IQ.class));
578         PacketCollector collector = mConnection.createPacketCollector(filter);
579         mConnection.sendPacket(req);
580         IQ result = (IQ)collector.nextResult(PING_TIMEOUT);
581         if (result == null)
582         {
583                 Log.e(TAG, "ping timeout");
584                 return false;
585         }
586         collector.cancel();
587         return true;
588         }
589         
590         /**
591          * A TimerTask that keeps connections to the server alive by sending a space
592          * character on an interval.
593          */
594         class PingTask implements Runnable {
595
596                 private static final int INITIAL_PING_DELAY = 15000;
597                 private long delay;
598                 private Thread thread;
599
600                 public PingTask(long delay) {
601                         this.delay = delay;
602                 }
603
604                 protected void setThread(Thread thread) {
605                         this.thread = thread;
606                 }
607
608                 public void run() {
609                         try {
610                                 // Sleep 15 seconds before sending first heartbeat. This will give time to
611                                 // properly finish TLS negotiation and then start sending heartbeats.
612                                 Thread.sleep(INITIAL_PING_DELAY);
613                         }
614                         catch (InterruptedException ie) {
615                                 // Do nothing
616                         }
617                         while (mConnection != null && pingThread == thread) {
618                                 Log.d(TAG, "ping");
619                                 if (mConnection.isConnected() && !sendPing()) {
620                                         Log.i(TAG, "ping failed");
621                                         mConnection.force_shutdown();
622                                 }
623                                 try {
624                                         // Sleep until we should write the next keep-alive.
625                                         Thread.sleep(delay);
626                                 }
627                                 catch (InterruptedException ie) {
628                                         // Do nothing
629                                 }
630                         }
631                 }
632         }
633         
634         static class MyXMPPConnection extends XMPPConnection {
635
636                 public MyXMPPConnection(ConnectionConfiguration config) {
637                         super(config);
638                 }
639
640         }
641 }
642