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