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 if (message.getConversation().getMode()==Conversation.MODE_SINGLE) {
198 databaseBackend.createMessage(message);
199 }
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 if (message.getConversation().getMode()==Conversation.MODE_SINGLE) {
212 message.setStatus(Message.STATUS_SEND);
213 databaseBackend.updateMessage(message);
214 }
215 }
216 }
217
218 public void getRoster(Account account,
219 final OnRosterFetchedListener listener) {
220 List<Contact> contacts = databaseBackend.getContacts(account);
221 for (int i = 0; i < contacts.size(); ++i) {
222 contacts.get(i).setAccount(account);
223 }
224 if (listener != null) {
225 listener.onRosterFetched(contacts);
226 }
227 }
228
229 public void updateRoster(final Account account,
230 final OnRosterFetchedListener listener) {
231
232 final Hashtable<String, Bundle> phoneContacts = new Hashtable<String, Bundle>();
233 final List<Contact> contacts = new ArrayList<Contact>();
234
235 final String[] PROJECTION = new String[] {
236 ContactsContract.Data.CONTACT_ID,
237 ContactsContract.Data.DISPLAY_NAME,
238 ContactsContract.Data.PHOTO_THUMBNAIL_URI,
239 ContactsContract.CommonDataKinds.Im.DATA };
240
241 final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
242 + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
243 + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
244 + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
245 + "\")";
246
247 CursorLoader mCursorLoader = new CursorLoader(this,
248 ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null,
249 null);
250 mCursorLoader.registerListener(0, new OnLoadCompleteListener<Cursor>() {
251
252 @Override
253 public void onLoadComplete(Loader<Cursor> arg0, Cursor cursor) {
254 while (cursor.moveToNext()) {
255 Bundle contact = new Bundle();
256 contact.putInt("phoneid", cursor.getInt(cursor
257 .getColumnIndex(ContactsContract.Data.CONTACT_ID)));
258 contact.putString(
259 "displayname",
260 cursor.getString(cursor
261 .getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
262 contact.putString(
263 "photouri",
264 cursor.getString(cursor
265 .getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI)));
266 phoneContacts.put(
267 cursor.getString(cursor
268 .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)),
269 contact);
270 }
271 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
272 Element query = new Element("query");
273 query.setAttribute("xmlns", "jabber:iq:roster");
274 query.setAttribute("ver", "");
275 iqPacket.addChild(query);
276 connections.get(account).sendIqPacket(iqPacket,
277 new OnIqPacketReceived() {
278
279 @Override
280 public void onIqPacketReceived(Account account,
281 IqPacket packet) {
282 Element roster = packet.findChild("query");
283 if (roster != null) {
284 for (Element item : roster.getChildren()) {
285 Contact contact;
286 String name = item.getAttribute("name");
287 String jid = item.getAttribute("jid");
288 if (phoneContacts.containsKey(jid)) {
289 Bundle phoneContact = phoneContacts
290 .get(jid);
291 contact = new Contact(
292 account,
293 phoneContact
294 .getString("displayname"),
295 jid,
296 phoneContact
297 .getString("photouri"));
298 contact.setSystemAccount(phoneContact
299 .getInt("phoneid"));
300 } else {
301 if (name == null) {
302 name = jid.split("@")[0];
303 }
304 contact = new Contact(account,
305 name, jid, null);
306
307 }
308 contact.setAccount(account);
309 contact.setSubscription(item
310 .getAttribute("subscription"));
311 contacts.add(contact);
312 }
313 databaseBackend.mergeContacts(contacts);
314 if (listener != null) {
315 listener.onRosterFetched(contacts);
316 }
317 }
318 }
319 });
320
321 }
322 });
323 mCursorLoader.startLoading();
324 }
325
326 public void addConversation(Conversation conversation) {
327 databaseBackend.createConversation(conversation);
328 }
329
330 public List<Conversation> getConversations() {
331 if (this.conversations == null) {
332 Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
333 for (Account account : this.accounts) {
334 accountLookupTable.put(account.getUuid(), account);
335 }
336 this.conversations = databaseBackend
337 .getConversations(Conversation.STATUS_AVAILABLE);
338 for (Conversation conv : this.conversations) {
339 conv.setAccount(accountLookupTable.get(conv.getAccountUuid()));
340 }
341 }
342 return this.conversations;
343 }
344
345 public List<Account> getAccounts() {
346 return this.accounts;
347 }
348
349 public List<Message> getMessages(Conversation conversation) {
350 return databaseBackend.getMessages(conversation, 100);
351 }
352
353 public Contact findOrCreateContact(Account account, String jid) {
354 Contact contact = databaseBackend.findContact(account, jid);
355 if (contact != null) {
356 contact.setAccount(account);
357 return contact;
358 } else {
359 return new Contact(account, jid.split("@")[0], jid, null);
360 }
361 }
362
363 public Conversation findOrCreateConversation(Account account,
364 Contact contact, boolean muc) {
365 for (Conversation conv : this.getConversations()) {
366 if ((conv.getAccount().equals(account))
367 && (conv.getContactJid().equals(contact.getJid()))) {
368 return conv;
369 }
370 }
371 Conversation conversation = databaseBackend.findConversation(account,
372 contact.getJid());
373 if (conversation != null) {
374 conversation.setStatus(Conversation.STATUS_AVAILABLE);
375 conversation.setAccount(account);
376 if (muc) {
377 conversation.setMode(Conversation.MODE_MULTI);
378 if (account.getStatus() == Account.STATUS_ONLINE) {
379 joinMuc(account, conversation);
380 }
381 } else {
382 conversation.setMode(Conversation.MODE_SINGLE);
383 }
384 this.databaseBackend.updateConversation(conversation);
385 } else {
386 if (muc) {
387 conversation = new Conversation(contact.getDisplayName(),
388 contact.getProfilePhoto(), account, contact.getJid(),
389 Conversation.MODE_MULTI);
390 if (account.getStatus() == Account.STATUS_ONLINE) {
391 joinMuc(account, conversation);
392 }
393 } else {
394 conversation = new Conversation(contact.getDisplayName(),
395 contact.getProfilePhoto(), account, contact.getJid(),
396 Conversation.MODE_SINGLE);
397 }
398 this.databaseBackend.createConversation(conversation);
399 }
400 this.conversations.add(conversation);
401 if (this.convChangedListener != null) {
402 this.convChangedListener.onConversationListChanged();
403 }
404 return conversation;
405 }
406
407 public void archiveConversation(Conversation conversation) {
408 this.databaseBackend.updateConversation(conversation);
409 this.conversations.remove(conversation);
410 if (this.convChangedListener != null) {
411 this.convChangedListener.onConversationListChanged();
412 }
413 }
414
415 public int getConversationCount() {
416 return this.databaseBackend.getConversationCount();
417 }
418
419 public void createAccount(Account account) {
420 databaseBackend.createAccount(account);
421 this.accounts.add(account);
422 this.connections.put(account, this.createConnection(account));
423 if (accountChangedListener != null)
424 accountChangedListener.onAccountListChangedListener();
425 }
426
427 public void updateAccount(Account account) {
428 databaseBackend.updateAccount(account);
429 XmppConnection connection = this.connections.get(account);
430 if (connection != null) {
431 connection.disconnect();
432 this.connections.remove(account);
433 }
434 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
435 this.connections.put(account, this.createConnection(account));
436 } else {
437 Log.d(LOGTAG, account.getJid()
438 + ": not starting because it's disabled");
439 }
440 if (accountChangedListener != null)
441 accountChangedListener.onAccountListChangedListener();
442 }
443
444 public void deleteAccount(Account account) {
445 Log.d(LOGTAG, "called delete account");
446 if (this.connections.containsKey(account)) {
447 Log.d(LOGTAG, "found connection. disconnecting");
448 this.connections.get(account).disconnect();
449 this.connections.remove(account);
450 this.accounts.remove(account);
451 }
452 databaseBackend.deleteAccount(account);
453 if (accountChangedListener != null)
454 accountChangedListener.onAccountListChangedListener();
455 }
456
457 public void setOnConversationListChangedListener(
458 OnConversationListChangedListener listener) {
459 this.convChangedListener = listener;
460 }
461
462 public void removeOnConversationListChangedListener() {
463 this.convChangedListener = null;
464 }
465
466 public void setOnAccountListChangedListener(
467 OnAccountListChangedListener listener) {
468 this.accountChangedListener = listener;
469 }
470
471 public void removeOnAccountListChangedListener() {
472 this.accountChangedListener = null;
473 }
474
475 public void connectMultiModeConversations(Account account) {
476 List<Conversation> conversations = getConversations();
477 for (int i = 0; i < conversations.size(); i++) {
478 Conversation conversation = conversations.get(i);
479 if ((conversation.getMode() == Conversation.MODE_MULTI)
480 && (conversation.getAccount() == account)) {
481 joinMuc(account, conversation);
482 }
483 }
484 }
485
486 public void joinMuc(Account account, Conversation conversation) {
487 String muc = conversation.getContactJid();
488 PresencePacket packet = new PresencePacket();
489 packet.setAttribute("to", muc + "/" + account.getUsername());
490 Element x = new Element("x");
491 x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
492 packet.addChild(x);
493 connections.get(conversation.getAccount()).sendPresencePacket(packet);
494 }
495
496 public void disconnectMultiModeConversations() {
497
498 }
499
500 @Override
501 public IBinder onBind(Intent intent) {
502 return mBinder;
503 }
504}