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