1package de.gultsch.chat.services;
2
3import java.util.ArrayList;
4import java.util.Hashtable;
5import java.util.List;
6
7import de.gultsch.chat.entities.Account;
8import de.gultsch.chat.entities.Contact;
9import de.gultsch.chat.entities.Conversation;
10import de.gultsch.chat.entities.Message;
11import de.gultsch.chat.entities.Presences;
12import de.gultsch.chat.persistance.DatabaseBackend;
13import de.gultsch.chat.ui.OnAccountListChangedListener;
14import de.gultsch.chat.ui.OnConversationListChangedListener;
15import de.gultsch.chat.ui.OnRosterFetchedListener;
16import de.gultsch.chat.utils.OnPhoneContactsLoadedListener;
17import de.gultsch.chat.utils.PhoneHelper;
18import de.gultsch.chat.utils.UIHelper;
19import de.gultsch.chat.xml.Element;
20import de.gultsch.chat.xmpp.IqPacket;
21import de.gultsch.chat.xmpp.MessagePacket;
22import de.gultsch.chat.xmpp.OnIqPacketReceived;
23import de.gultsch.chat.xmpp.OnMessagePacketReceived;
24import de.gultsch.chat.xmpp.OnPresencePacketReceived;
25import de.gultsch.chat.xmpp.OnStatusChanged;
26import de.gultsch.chat.xmpp.PresencePacket;
27import de.gultsch.chat.xmpp.XmppConnection;
28import android.app.NotificationManager;
29import android.app.Service;
30import android.content.Context;
31import android.content.CursorLoader;
32import android.content.Intent;
33import android.content.Loader;
34import android.content.Loader.OnLoadCompleteListener;
35import android.database.ContentObserver;
36import android.database.Cursor;
37import android.os.Binder;
38import android.os.Bundle;
39import android.os.IBinder;
40import android.os.PowerManager;
41import android.provider.ContactsContract;
42import android.util.Log;
43
44public class XmppConnectionService extends Service {
45
46 protected static final String LOGTAG = "xmppService";
47 protected DatabaseBackend databaseBackend;
48
49 public long startDate;
50
51 private List<Account> accounts;
52 private List<Conversation> conversations = null;
53
54 private Hashtable<Account, XmppConnection> connections = new Hashtable<Account, XmppConnection>();
55
56 private OnConversationListChangedListener convChangedListener = null;
57 private OnAccountListChangedListener accountChangedListener = null;
58
59 private ContentObserver contactObserver = new ContentObserver(null) {
60 @Override
61 public void onChange(boolean selfChange) {
62 super.onChange(selfChange);
63 Log.d(LOGTAG,"contact list has changed");
64 mergePhoneContactsWithRoster();
65 }
66 };
67
68 private final IBinder mBinder = new XmppConnectionBinder();
69 private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
70
71 @Override
72 public void onMessagePacketReceived(Account account,
73 MessagePacket packet) {
74 if ((packet.getType() == MessagePacket.TYPE_CHAT)
75 || (packet.getType() == MessagePacket.TYPE_GROUPCHAT)) {
76 boolean notify = true;
77 int status = Message.STATUS_RECIEVED;
78 String body;
79 String fullJid;
80 if (!packet.hasChild("body")) {
81 Element forwarded;
82 if (packet.hasChild("received")) {
83 forwarded = packet.findChild("received").findChild(
84 "forwarded");
85 } else if (packet.hasChild("sent")) {
86 forwarded = packet.findChild("sent").findChild(
87 "forwarded");
88 status = Message.STATUS_SEND;
89 notify = false;
90 } else {
91 return; // massage has no body and is not carbon. just
92 // skip
93 }
94 if (forwarded != null) {
95 Element message = forwarded.findChild("message");
96 if ((message == null) || (!message.hasChild("body")))
97 return; // either malformed or boring
98 if (status == Message.STATUS_RECIEVED) {
99 fullJid = message.getAttribute("from");
100 } else {
101 fullJid = message.getAttribute("to");
102 }
103 body = message.findChild("body").getContent();
104 } else {
105 return; // packet malformed. has no forwarded element
106 }
107 } else {
108 fullJid = packet.getFrom();
109 body = packet.getBody();
110 }
111 Conversation conversation = null;
112 String[] fromParts = fullJid.split("/");
113 String jid = fromParts[0];
114 boolean muc = (packet.getType() == MessagePacket.TYPE_GROUPCHAT);
115 String counterPart = null;
116 conversation = findOrCreateConversation(account, jid, muc);
117 if (muc) {
118 if ((fromParts.length == 1) || (packet.hasChild("subject"))
119 || (packet.hasChild("delay"))) {
120 return;
121 }
122 counterPart = fromParts[1];
123 if (counterPart.equals(account.getUsername())) {
124 status = Message.STATUS_SEND;
125 notify = false;
126 }
127 } else {
128 counterPart = fullJid;
129 }
130 Message message = new Message(conversation, counterPart, body,
131 Message.ENCRYPTION_NONE, status);
132 if(notify) {
133 message.markUnread();
134 }
135 conversation.getMessages().add(message);
136 databaseBackend.createMessage(message);
137 if (convChangedListener != null) {
138 convChangedListener.onConversationListChanged();
139 } else {
140 if (notify) {
141 NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
142 mNotificationManager.notify(2342, UIHelper
143 .getUnreadMessageNotification(
144 getApplicationContext(), conversation));
145 }
146 }
147 }
148 }
149 };
150 private OnStatusChanged statusListener = new OnStatusChanged() {
151
152 @Override
153 public void onStatusChanged(Account account) {
154 if (accountChangedListener != null) {
155 accountChangedListener.onAccountListChangedListener();
156 }
157 if (account.getStatus() == Account.STATUS_ONLINE) {
158 databaseBackend.clearPresences(account);
159 connectMultiModeConversations(account);
160 }
161 }
162 };
163
164 private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
165
166 @Override
167 public void onPresencePacketReceived(Account account,
168 PresencePacket packet) {
169 String[] fromParts = packet.getAttribute("from").split("/");
170 Contact contact = findContact(account, fromParts[0]);
171 if (contact == null) {
172 // most likely muc, self or roster not synced
173 // Log.d(LOGTAG,"got presence for non contact "+packet.toString());
174 return;
175 }
176 String type = packet.getAttribute("type");
177 if (type == null) {
178 Element show = packet.findChild("show");
179 if (show == null) {
180 contact.updatePresence(fromParts[1], Presences.ONLINE);
181 } else if (show.getContent().equals("away")) {
182 contact.updatePresence(fromParts[1], Presences.AWAY);
183 } else if (show.getContent().equals("xa")) {
184 contact.updatePresence(fromParts[1], Presences.XA);
185 } else if (show.getContent().equals("chat")) {
186 contact.updatePresence(fromParts[1], Presences.CHAT);
187 } else if (show.getContent().equals("dnd")) {
188 contact.updatePresence(fromParts[1], Presences.DND);
189 }
190 databaseBackend.updateContact(contact);
191 } else if (type.equals("unavailable")) {
192 if (fromParts.length != 2) {
193 // Log.d(LOGTAG,"received presence with no resource "+packet.toString());
194 } else {
195 contact.removePresence(fromParts[1]);
196 databaseBackend.updateContact(contact);
197 }
198 }
199 }
200 };
201
202 public class XmppConnectionBinder extends Binder {
203 public XmppConnectionService getService() {
204 return XmppConnectionService.this;
205 }
206 }
207
208 @Override
209 public int onStartCommand(Intent intent, int flags, int startId) {
210 for (Account account : accounts) {
211 if (!connections.containsKey(account)) {
212 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
213 this.connections.put(account,
214 this.createConnection(account));
215 } else {
216 Log.d(LOGTAG, account.getJid()
217 + ": not starting because it's disabled");
218 }
219 }
220 }
221 return START_STICKY;
222 }
223
224 @Override
225 public void onCreate() {
226 databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
227 this.accounts = databaseBackend.getAccounts();
228
229 getContentResolver()
230 .registerContentObserver(
231 ContactsContract.Contacts.CONTENT_URI, true,contactObserver);
232 }
233
234 public XmppConnection createConnection(Account account) {
235 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
236 XmppConnection connection = new XmppConnection(account, pm);
237 connection.setOnMessagePacketReceivedListener(this.messageListener);
238 connection.setOnStatusChangedListener(this.statusListener);
239 connection.setOnPresencePacketReceivedListener(this.presenceListener);
240 Thread thread = new Thread(connection);
241 thread.start();
242 return connection;
243 }
244
245 public void sendMessage(final Account account, final Message message) {
246 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
247 databaseBackend.createMessage(message);
248 }
249 MessagePacket packet = new MessagePacket();
250 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
251 packet.setType(MessagePacket.TYPE_CHAT);
252 } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
253 packet.setType(MessagePacket.TYPE_GROUPCHAT);
254 }
255 packet.setTo(message.getCounterpart());
256 packet.setFrom(account.getJid());
257 packet.setBody(message.getBody());
258 if (account.getStatus() == Account.STATUS_ONLINE) {
259 connections.get(account).sendMessagePacket(packet);
260 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
261 message.setStatus(Message.STATUS_SEND);
262 databaseBackend.updateMessage(message);
263 }
264 }
265 }
266
267 public void getRoster(Account account,
268 final OnRosterFetchedListener listener) {
269 List<Contact> contacts = databaseBackend.getContacts(account);
270 for (int i = 0; i < contacts.size(); ++i) {
271 contacts.get(i).setAccount(account);
272 }
273 if (listener != null) {
274 listener.onRosterFetched(contacts);
275 }
276 }
277
278 public void updateRoster(final Account account,
279 final OnRosterFetchedListener listener) {
280
281 PhoneHelper.loadPhoneContacts(this,
282 new OnPhoneContactsLoadedListener() {
283
284 @Override
285 public void onPhoneContactsLoaded(
286 final Hashtable<String, Bundle> phoneContacts) {
287 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
288 Element query = new Element("query");
289 query.setAttribute("xmlns", "jabber:iq:roster");
290 query.setAttribute("ver", "");
291 iqPacket.addChild(query);
292 connections.get(account).sendIqPacket(iqPacket,
293 new OnIqPacketReceived() {
294
295 @Override
296 public void onIqPacketReceived(
297 Account account, IqPacket packet) {
298 List<Contact> contacts = new ArrayList<Contact>();
299 Element roster = packet
300 .findChild("query");
301 if (roster != null) {
302 for (Element item : roster
303 .getChildren()) {
304 Contact contact;
305 String name = item
306 .getAttribute("name");
307 String jid = item
308 .getAttribute("jid");
309 if (phoneContacts
310 .containsKey(jid)) {
311 Bundle phoneContact = phoneContacts
312 .get(jid);
313 String systemAccount = phoneContact
314 .getInt("phoneid")
315 + "#"
316 + phoneContact
317 .getString("lookup");
318 contact = new Contact(
319 account,
320 phoneContact
321 .getString("displayname"),
322 jid,
323 phoneContact
324 .getString("photouri"));
325 contact.setSystemAccount(systemAccount);
326 } else {
327 if (name == null) {
328 name = jid.split("@")[0];
329 }
330 contact = new Contact(
331 account, name, jid,
332 null);
333
334 }
335 contact.setAccount(account);
336 contact.setSubscription(item
337 .getAttribute("subscription"));
338 contacts.add(contact);
339 }
340 databaseBackend
341 .mergeContacts(contacts);
342 if (listener != null) {
343 listener.onRosterFetched(contacts);
344 }
345 }
346 }
347 });
348
349 }
350 });
351 }
352
353 public void mergePhoneContactsWithRoster() {
354 PhoneHelper.loadPhoneContacts(this,
355 new OnPhoneContactsLoadedListener() {
356 @Override
357 public void onPhoneContactsLoaded(
358 Hashtable<String, Bundle> phoneContacts) {
359 List<Contact> contacts = databaseBackend
360 .getContacts(null);
361 for (int i = 0; i < contacts.size(); ++i) {
362 Contact contact = contacts.get(i);
363 if (phoneContacts.containsKey(contact.getJid())) {
364 Bundle phoneContact = phoneContacts.get(contact
365 .getJid());
366 String systemAccount = phoneContact
367 .getInt("phoneid")
368 + "#"
369 + phoneContact.getString("lookup");
370 contact.setSystemAccount(systemAccount);
371 contact.setPhotoUri(phoneContact
372 .getString("photouri"));
373 contact.setDisplayName(phoneContact
374 .getString("displayname"));
375 databaseBackend.updateContact(contact);
376 } else {
377 if ((contact.getSystemAccount() != null)
378 || (contact.getProfilePhoto() != null)) {
379 contact.setSystemAccount(null);
380 contact.setPhotoUri(null);
381 databaseBackend.updateContact(contact);
382 }
383 }
384 }
385 }
386 });
387 }
388
389 public void addConversation(Conversation conversation) {
390 databaseBackend.createConversation(conversation);
391 }
392
393 public List<Conversation> getConversations() {
394 if (this.conversations == null) {
395 Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
396 for (Account account : this.accounts) {
397 accountLookupTable.put(account.getUuid(), account);
398 }
399 this.conversations = databaseBackend
400 .getConversations(Conversation.STATUS_AVAILABLE);
401 for (Conversation conv : this.conversations) {
402 Account account = accountLookupTable.get(conv.getAccountUuid());
403 conv.setAccount(account);
404 conv.setContact(findContact(account, conv.getContactJid()));
405 conv.setMessages(databaseBackend.getMessages(conv, 50));
406 }
407 }
408 return this.conversations;
409 }
410
411 public List<Account> getAccounts() {
412 return this.accounts;
413 }
414
415 public Contact findContact(Account account, String jid) {
416 return databaseBackend.findContact(account, jid);
417 }
418
419 public Conversation findOrCreateConversation(Account account,
420 String jid, boolean muc) {
421 for (Conversation conv : this.getConversations()) {
422 if ((conv.getAccount().equals(account))
423 && (conv.getContactJid().equals(jid))) {
424 return conv;
425 }
426 }
427 Conversation conversation = databaseBackend.findConversation(account,jid);
428 if (conversation != null) {
429 conversation.setStatus(Conversation.STATUS_AVAILABLE);
430 conversation.setAccount(account);
431 if (muc) {
432 conversation.setMode(Conversation.MODE_MULTI);
433 if (account.getStatus() == Account.STATUS_ONLINE) {
434 joinMuc(account, conversation);
435 }
436 } else {
437 conversation.setMode(Conversation.MODE_SINGLE);
438 }
439 this.databaseBackend.updateConversation(conversation);
440 } else {
441 String conversationName;
442 Contact contact = findContact(account, jid);
443 if (contact!=null) {
444 conversationName = contact.getDisplayName();
445 } else {
446 conversationName = jid.split("@")[0];
447 }
448 if (muc) {
449 conversation = new Conversation(conversationName,account, jid,
450 Conversation.MODE_MULTI);
451 if (account.getStatus() == Account.STATUS_ONLINE) {
452 joinMuc(account, conversation);
453 }
454 } else {
455 conversation = new Conversation(conversationName, account, jid,
456 Conversation.MODE_SINGLE);
457 }
458 conversation.setContact(contact);
459 this.databaseBackend.createConversation(conversation);
460 }
461 this.conversations.add(conversation);
462 if (this.convChangedListener != null) {
463 this.convChangedListener.onConversationListChanged();
464 }
465 return conversation;
466 }
467
468 public void archiveConversation(Conversation conversation) {
469 this.databaseBackend.updateConversation(conversation);
470 this.conversations.remove(conversation);
471 if (this.convChangedListener != null) {
472 this.convChangedListener.onConversationListChanged();
473 }
474 }
475
476 public int getConversationCount() {
477 return this.databaseBackend.getConversationCount();
478 }
479
480 public void createAccount(Account account) {
481 databaseBackend.createAccount(account);
482 this.accounts.add(account);
483 this.connections.put(account, this.createConnection(account));
484 if (accountChangedListener != null)
485 accountChangedListener.onAccountListChangedListener();
486 }
487
488 public void updateAccount(Account account) {
489 databaseBackend.updateAccount(account);
490 XmppConnection connection = this.connections.get(account);
491 if (connection != null) {
492 connection.disconnect();
493 this.connections.remove(account);
494 }
495 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
496 this.connections.put(account, this.createConnection(account));
497 } else {
498 Log.d(LOGTAG, account.getJid()
499 + ": not starting because it's disabled");
500 }
501 if (accountChangedListener != null)
502 accountChangedListener.onAccountListChangedListener();
503 }
504
505 public void deleteAccount(Account account) {
506 Log.d(LOGTAG, "called delete account");
507 if (this.connections.containsKey(account)) {
508 Log.d(LOGTAG, "found connection. disconnecting");
509 this.connections.get(account).disconnect();
510 this.connections.remove(account);
511 this.accounts.remove(account);
512 }
513 databaseBackend.deleteAccount(account);
514 if (accountChangedListener != null)
515 accountChangedListener.onAccountListChangedListener();
516 }
517
518 public void setOnConversationListChangedListener(
519 OnConversationListChangedListener listener) {
520 this.convChangedListener = listener;
521 }
522
523 public void removeOnConversationListChangedListener() {
524 this.convChangedListener = null;
525 }
526
527 public void setOnAccountListChangedListener(
528 OnAccountListChangedListener listener) {
529 this.accountChangedListener = listener;
530 }
531
532 public void removeOnAccountListChangedListener() {
533 this.accountChangedListener = null;
534 }
535
536 public void connectMultiModeConversations(Account account) {
537 List<Conversation> conversations = getConversations();
538 for (int i = 0; i < conversations.size(); i++) {
539 Conversation conversation = conversations.get(i);
540 if ((conversation.getMode() == Conversation.MODE_MULTI)
541 && (conversation.getAccount() == account)) {
542 joinMuc(account, conversation);
543 }
544 }
545 }
546
547 public void joinMuc(Account account, Conversation conversation) {
548 String muc = conversation.getContactJid();
549 PresencePacket packet = new PresencePacket();
550 packet.setAttribute("to", muc + "/" + account.getUsername());
551 Element x = new Element("x");
552 x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
553 packet.addChild(x);
554 connections.get(conversation.getAccount()).sendPresencePacket(packet);
555 }
556
557 public void disconnectMultiModeConversations() {
558
559 }
560
561 @Override
562 public IBinder onBind(Intent intent) {
563 return mBinder;
564 }
565}