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