1package eu.siacs.conversations.services;
2
3import android.annotation.SuppressLint;
4import android.app.AlarmManager;
5import android.app.PendingIntent;
6import android.app.Service;
7import android.content.Context;
8import android.content.Intent;
9import android.content.SharedPreferences;
10import android.database.ContentObserver;
11import android.graphics.Bitmap;
12import android.net.ConnectivityManager;
13import android.net.NetworkInfo;
14import android.net.Uri;
15import android.os.Binder;
16import android.os.Bundle;
17import android.os.FileObserver;
18import android.os.IBinder;
19import android.os.PowerManager;
20import android.os.PowerManager.WakeLock;
21import android.os.SystemClock;
22import android.preference.PreferenceManager;
23import android.provider.ContactsContract;
24import android.util.Log;
25import android.util.LruCache;
26
27import net.java.otr4j.OtrException;
28import net.java.otr4j.session.Session;
29import net.java.otr4j.session.SessionID;
30import net.java.otr4j.session.SessionStatus;
31
32import org.openintents.openpgp.util.OpenPgpApi;
33import org.openintents.openpgp.util.OpenPgpServiceConnection;
34
35import java.io.FileNotFoundException;
36import java.io.IOException;
37import java.io.OutputStream;
38import java.security.SecureRandom;
39import java.text.SimpleDateFormat;
40import java.util.ArrayList;
41import java.util.Collections;
42import java.util.Comparator;
43import java.util.Date;
44import java.util.Hashtable;
45import java.util.List;
46import java.util.Locale;
47import java.util.TimeZone;
48import java.util.concurrent.CopyOnWriteArrayList;
49
50import de.duenndns.ssl.MemorizingTrustManager;
51import eu.siacs.conversations.Config;
52import eu.siacs.conversations.R;
53import eu.siacs.conversations.crypto.PgpEngine;
54import eu.siacs.conversations.entities.Account;
55import eu.siacs.conversations.entities.Bookmark;
56import eu.siacs.conversations.entities.Contact;
57import eu.siacs.conversations.entities.Conversation;
58import eu.siacs.conversations.entities.Downloadable;
59import eu.siacs.conversations.entities.Message;
60import eu.siacs.conversations.entities.MucOptions;
61import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
62import eu.siacs.conversations.entities.Presences;
63import eu.siacs.conversations.generator.IqGenerator;
64import eu.siacs.conversations.generator.MessageGenerator;
65import eu.siacs.conversations.generator.PresenceGenerator;
66import eu.siacs.conversations.http.HttpConnectionManager;
67import eu.siacs.conversations.parser.IqParser;
68import eu.siacs.conversations.parser.MessageParser;
69import eu.siacs.conversations.parser.PresenceParser;
70import eu.siacs.conversations.persistance.DatabaseBackend;
71import eu.siacs.conversations.persistance.FileBackend;
72import eu.siacs.conversations.ui.UiCallback;
73import eu.siacs.conversations.utils.CryptoHelper;
74import eu.siacs.conversations.utils.ExceptionHelper;
75import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
76import eu.siacs.conversations.utils.PRNGFixes;
77import eu.siacs.conversations.utils.PhoneHelper;
78import eu.siacs.conversations.utils.UIHelper;
79import eu.siacs.conversations.xml.Element;
80import eu.siacs.conversations.xmpp.OnBindListener;
81import eu.siacs.conversations.xmpp.OnContactStatusChanged;
82import eu.siacs.conversations.xmpp.OnIqPacketReceived;
83import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
84import eu.siacs.conversations.xmpp.OnStatusChanged;
85import eu.siacs.conversations.xmpp.XmppConnection;
86import eu.siacs.conversations.xmpp.jid.InvalidJidException;
87import eu.siacs.conversations.xmpp.jid.Jid;
88import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
89import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
90import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
91import eu.siacs.conversations.xmpp.pep.Avatar;
92import eu.siacs.conversations.xmpp.stanzas.IqPacket;
93import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
94import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
95
96public class XmppConnectionService extends Service {
97
98 public static String ACTION_CLEAR_NOTIFICATION = "clear_notification";
99 private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
100 public static String ACTION_DISABLE_FOREGROUND = "disable_foreground";
101 private ContentObserver contactObserver = new ContentObserver(null) {
102 @Override
103 public void onChange(boolean selfChange) {
104 super.onChange(selfChange);
105 Intent intent = new Intent(getApplicationContext(),
106 XmppConnectionService.class);
107 intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
108 startService(intent);
109 }
110 };
111 private final IBinder mBinder = new XmppConnectionBinder();
112 public DatabaseBackend databaseBackend;
113 public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
114
115 @Override
116 public void onContactStatusChanged(Contact contact, boolean online) {
117 Conversation conversation = find(getConversations(), contact);
118 if (conversation != null) {
119 if (online && contact.getPresences().size() > 1) {
120 conversation.endOtrIfNeeded();
121 } else {
122 conversation.resetOtrSession();
123 }
124 if (online && (contact.getPresences().size() == 1)) {
125 sendUnsendMessages(conversation);
126 }
127 }
128 }
129 };
130 private FileBackend fileBackend = new FileBackend(this);
131 private MemorizingTrustManager mMemorizingTrustManager;
132 private NotificationService mNotificationService = new NotificationService(
133 this);
134 private MessageParser mMessageParser = new MessageParser(this);
135 private PresenceParser mPresenceParser = new PresenceParser(this);
136 private IqParser mIqParser = new IqParser(this);
137 private MessageGenerator mMessageGenerator = new MessageGenerator(this);
138 private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
139 private List<Account> accounts;
140 private CopyOnWriteArrayList<Conversation> conversations = null;
141 private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
142 this);
143 private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
144 this);
145 private AvatarService mAvatarService = new AvatarService(this);
146 private OnConversationUpdate mOnConversationUpdate = null;
147 private Integer convChangedListenerCount = 0;
148 private OnAccountUpdate mOnAccountUpdate = null;
149 private OnStatusChanged statusListener = new OnStatusChanged() {
150
151 @Override
152 public void onStatusChanged(Account account) {
153 XmppConnection connection = account.getXmppConnection();
154 if (mOnAccountUpdate != null) {
155 mOnAccountUpdate.onAccountUpdate();
156 }
157 if (account.getStatus() == Account.STATUS_ONLINE) {
158 for (Conversation conversation : account.pendingConferenceLeaves) {
159 leaveMuc(conversation);
160 }
161 for (Conversation conversation : account.pendingConferenceJoins) {
162 joinMuc(conversation);
163 }
164 mJingleConnectionManager.cancelInTransmission();
165 List<Conversation> conversations = getConversations();
166 for (Conversation conversation : conversations) {
167 if (conversation.getAccount() == account) {
168 conversation.startOtrIfNeeded();
169 sendUnsendMessages(conversation);
170 }
171 }
172 if (connection != null && connection.getFeatures().csi()) {
173 if (checkListeners()) {
174 Log.d(Config.LOGTAG, account.getJid().toBareJid()
175 + " sending csi//inactive");
176 connection.sendInactive();
177 } else {
178 Log.d(Config.LOGTAG, account.getJid().toBareJid()
179 + " sending csi//active");
180 connection.sendActive();
181 }
182 }
183 syncDirtyContacts(account);
184 scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
185 } else if (account.getStatus() == Account.STATUS_OFFLINE) {
186 resetSendingToWaiting(account);
187 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
188 int timeToReconnect = mRandom.nextInt(50) + 10;
189 scheduleWakeupCall(timeToReconnect, false);
190 }
191 } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
192 databaseBackend.updateAccount(account);
193 reconnectAccount(account, true);
194 } else if ((account.getStatus() != Account.STATUS_CONNECTING)
195 && (account.getStatus() != Account.STATUS_NO_INTERNET)) {
196 if (connection != null) {
197 int next = connection.getTimeToNextAttempt();
198 Log.d(Config.LOGTAG, account.getJid().toBareJid()
199 + ": error connecting account. try again in "
200 + next + "s for the "
201 + (connection.getAttempt() + 1) + " time");
202 scheduleWakeupCall((int) (next * 1.2), false);
203 }
204 }
205 UIHelper.showErrorNotification(getApplicationContext(),
206 getAccounts());
207 }
208 };
209 private Integer accountChangedListenerCount = 0;
210 private OnRosterUpdate mOnRosterUpdate = null;
211 private Integer rosterChangedListenerCount = 0;
212 private SecureRandom mRandom;
213 private FileObserver fileObserver = new FileObserver(
214 FileBackend.getConversationsDirectory()) {
215
216 @Override
217 public void onEvent(int event, String path) {
218 if (event == FileObserver.DELETE) {
219 markFileDeleted(path.split("\\.")[0]);
220 }
221 }
222 };
223 private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
224
225 @Override
226 public void onJinglePacketReceived(Account account, JinglePacket packet) {
227 mJingleConnectionManager.deliverPacket(account, packet);
228 }
229 };
230
231 private OpenPgpServiceConnection pgpServiceConnection;
232 private PgpEngine mPgpEngine = null;
233 private Intent pingIntent;
234 private PendingIntent pendingPingIntent = null;
235 private WakeLock wakeLock;
236 private PowerManager pm;
237 private OnBindListener mOnBindListener = new OnBindListener() {
238
239 @Override
240 public void onBind(final Account account) {
241 account.getRoster().clearPresences();
242 account.clearPresences(); // self presences
243 account.pendingConferenceJoins.clear();
244 account.pendingConferenceLeaves.clear();
245 fetchRosterFromServer(account);
246 fetchBookmarks(account);
247 sendPresencePacket(account,
248 mPresenceGenerator.sendPresence(account));
249 connectMultiModeConversations(account);
250 updateConversationUi();
251 }
252 };
253
254 private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
255
256 @Override
257 public void onMessageAcknowledged(Account account, String uuid) {
258 for (Conversation conversation : getConversations()) {
259 if (conversation.getAccount() == account) {
260 for (Message message : conversation.getMessages()) {
261 if ((message.getStatus() == Message.STATUS_UNSEND || message
262 .getStatus() == Message.STATUS_WAITING)
263 && message.getUuid().equals(uuid)) {
264 markMessage(message, Message.STATUS_SEND);
265 return;
266 }
267 }
268 }
269 }
270 }
271 };
272 private LruCache<String, Bitmap> mBitmapCache;
273 private OnRenameListener renameListener = null;
274 private IqGenerator mIqGenerator = new IqGenerator(this);
275
276 public PgpEngine getPgpEngine() {
277 if (pgpServiceConnection.isBound()) {
278 if (this.mPgpEngine == null) {
279 this.mPgpEngine = new PgpEngine(new OpenPgpApi(
280 getApplicationContext(),
281 pgpServiceConnection.getService()), this);
282 }
283 return mPgpEngine;
284 } else {
285 return null;
286 }
287
288 }
289
290 public FileBackend getFileBackend() {
291 return this.fileBackend;
292 }
293
294 public AvatarService getAvatarService() {
295 return this.mAvatarService;
296 }
297
298 public Message attachImageToConversation(final Conversation conversation,
299 final Uri uri, final UiCallback<Message> callback) {
300 final Message message;
301 if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
302 message = new Message(conversation, "",
303 Message.ENCRYPTION_DECRYPTED);
304 } else {
305 message = new Message(conversation, "",
306 conversation.getNextEncryption(forceEncryption()));
307 }
308 message.setCounterpart(conversation.getNextCounterpart());
309 message.setType(Message.TYPE_IMAGE);
310 message.setStatus(Message.STATUS_OFFERED);
311 new Thread(new Runnable() {
312
313 @Override
314 public void run() {
315 try {
316 getFileBackend().copyImageToPrivateStorage(message, uri);
317 if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
318 getPgpEngine().encrypt(message, callback);
319 } else {
320 callback.success(message);
321 }
322 } catch (FileBackend.ImageCopyException e) {
323 callback.error(e.getResId(), message);
324 }
325 }
326 }).start();
327 return message;
328 }
329
330 public Conversation find(Bookmark bookmark) {
331 return find(bookmark.getAccount(), bookmark.getJid());
332 }
333
334 public Conversation find(final Account account, final Jid jid) {
335 return find(getConversations(), account, jid);
336 }
337
338 @Override
339 public int onStartCommand(Intent intent, int flags, int startId) {
340 if (intent != null && intent.getAction() != null) {
341 if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) {
342 mergePhoneContactsWithRoster();
343 return START_STICKY;
344 } else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) {
345 logoutAndSave();
346 return START_NOT_STICKY;
347 } else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) {
348 mNotificationService.clear();
349 } else if (intent.getAction().equals(ACTION_DISABLE_FOREGROUND)) {
350 getPreferences().edit().putBoolean("keep_foreground_service",false).commit();
351 toggleForegroundService();
352 }
353 }
354 this.wakeLock.acquire();
355
356 for (Account account : accounts) {
357 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
358 if (!hasInternetConnection()) {
359 account.setStatus(Account.STATUS_NO_INTERNET);
360 if (statusListener != null) {
361 statusListener.onStatusChanged(account);
362 }
363 } else {
364 if (account.getStatus() == Account.STATUS_NO_INTERNET) {
365 account.setStatus(Account.STATUS_OFFLINE);
366 if (statusListener != null) {
367 statusListener.onStatusChanged(account);
368 }
369 }
370 if (account.getStatus() == Account.STATUS_ONLINE) {
371 long lastReceived = account.getXmppConnection()
372 .getLastPacketReceived();
373 long lastSent = account.getXmppConnection()
374 .getLastPingSent();
375 if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) {
376 Log.d(Config.LOGTAG, account.getJid()
377 + ": ping timeout");
378 this.reconnectAccount(account, true);
379 } else if (SystemClock.elapsedRealtime() - lastReceived >= Config.PING_MIN_INTERVAL * 1000) {
380 account.getXmppConnection().sendPing();
381 this.scheduleWakeupCall(2, false);
382 }
383 } else if (account.getStatus() == Account.STATUS_OFFLINE) {
384 if (account.getXmppConnection() == null) {
385 account.setXmppConnection(this
386 .createConnection(account));
387 }
388 new Thread(account.getXmppConnection()).start();
389 } else if ((account.getStatus() == Account.STATUS_CONNECTING)
390 && ((SystemClock.elapsedRealtime() - account
391 .getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) {
392 Log.d(Config.LOGTAG, account.getJid()
393 + ": time out during connect reconnecting");
394 reconnectAccount(account, true);
395 } else {
396 if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
397 reconnectAccount(account, true);
398 }
399 }
400 // in any case. reschedule wakup call
401 this.scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
402 }
403 if (mOnAccountUpdate != null) {
404 mOnAccountUpdate.onAccountUpdate();
405 }
406 }
407 }
408 /*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
409 if (!pm.isScreenOn()) {
410 removeStaleListeners();
411 }*/
412 if (wakeLock.isHeld()) {
413 try {
414 wakeLock.release();
415 } catch (final RuntimeException ignored) {
416 }
417 }
418 return START_STICKY;
419 }
420
421 public boolean hasInternetConnection() {
422 ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
423 .getSystemService(Context.CONNECTIVITY_SERVICE);
424 NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
425 return activeNetwork != null && activeNetwork.isConnected();
426 }
427
428 @SuppressLint("TrulyRandom")
429 @Override
430 public void onCreate() {
431 ExceptionHelper.init(getApplicationContext());
432 PRNGFixes.apply();
433 this.mRandom = new SecureRandom();
434 this.mMemorizingTrustManager = new MemorizingTrustManager(
435 getApplicationContext());
436
437 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
438 int cacheSize = maxMemory / 8;
439 this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
440 @Override
441 protected int sizeOf(String key, Bitmap bitmap) {
442 return bitmap.getByteCount() / 1024;
443 }
444 };
445
446 this.databaseBackend = DatabaseBackend
447 .getInstance(getApplicationContext());
448 this.accounts = databaseBackend.getAccounts();
449
450 for (Account account : this.accounts) {
451 this.databaseBackend.readRoster(account.getRoster());
452 }
453 this.mergePhoneContactsWithRoster();
454 this.getConversations();
455
456 getContentResolver().registerContentObserver(
457 ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
458 this.fileObserver.startWatching();
459 this.pgpServiceConnection = new OpenPgpServiceConnection(
460 getApplicationContext(), "org.sufficientlysecure.keychain");
461 this.pgpServiceConnection.bindToService();
462
463 this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
464 this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
465 "XmppConnectionService");
466 toggleForegroundService();
467 }
468
469 public void toggleForegroundService() {
470 if (getPreferences().getBoolean("keep_foreground_service",false)) {
471 startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
472 } else {
473 stopForeground(true);
474 }
475 }
476
477 @Override
478 public void onTaskRemoved(Intent rootIntent) {
479 super.onTaskRemoved(rootIntent);
480 if (!getPreferences().getBoolean("keep_foreground_service",false)) {
481 this.logoutAndSave();
482 }
483 }
484
485 private void logoutAndSave() {
486 for (Account account : accounts) {
487 databaseBackend.writeRoster(account.getRoster());
488 if (account.getXmppConnection() != null) {
489 disconnect(account, false);
490 }
491 }
492 Context context = getApplicationContext();
493 AlarmManager alarmManager = (AlarmManager) context
494 .getSystemService(Context.ALARM_SERVICE);
495 Intent intent = new Intent(context, EventReceiver.class);
496 alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
497 Log.d(Config.LOGTAG, "good bye");
498 stopSelf();
499 }
500
501 protected void scheduleWakeupCall(int seconds, boolean ping) {
502 long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
503 Context context = getApplicationContext();
504 AlarmManager alarmManager = (AlarmManager) context
505 .getSystemService(Context.ALARM_SERVICE);
506
507 if (ping) {
508 if (this.pingIntent == null) {
509 this.pingIntent = new Intent(context, EventReceiver.class);
510 this.pingIntent.setAction("ping");
511 this.pingIntent.putExtra("time", timeToWake);
512 this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
513 this.pingIntent, 0);
514 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
515 timeToWake, pendingPingIntent);
516 } else {
517 long scheduledTime = this.pingIntent.getLongExtra("time", 0);
518 if (scheduledTime < SystemClock.elapsedRealtime()
519 || (scheduledTime > timeToWake)) {
520 this.pingIntent.putExtra("time", timeToWake);
521 alarmManager.cancel(this.pendingPingIntent);
522 this.pendingPingIntent = PendingIntent.getBroadcast(
523 context, 0, this.pingIntent, 0);
524 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
525 timeToWake, pendingPingIntent);
526 }
527 }
528 } else {
529 Intent intent = new Intent(context, EventReceiver.class);
530 intent.setAction("ping_check");
531 PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
532 intent, 0);
533 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
534 alarmIntent);
535 }
536
537 }
538
539 public XmppConnection createConnection(Account account) {
540 SharedPreferences sharedPref = getPreferences();
541 account.setResource(sharedPref.getString("resource", "mobile")
542 .toLowerCase(Locale.getDefault()));
543 XmppConnection connection = new XmppConnection(account, this);
544 connection.setOnMessagePacketReceivedListener(this.mMessageParser);
545 connection.setOnStatusChangedListener(this.statusListener);
546 connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
547 connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
548 connection.setOnJinglePacketReceivedListener(this.jingleListener);
549 connection.setOnBindListener(this.mOnBindListener);
550 connection
551 .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
552 return connection;
553 }
554
555 public void sendMessage(Message message) {
556 Account account = message.getConversation().getAccount();
557 account.deactivateGracePeriod();
558 Conversation conv = message.getConversation();
559 MessagePacket packet = null;
560 boolean saveInDb = true;
561 boolean send = false;
562 if (account.getStatus() == Account.STATUS_ONLINE
563 && account.getXmppConnection() != null) {
564 if (message.getType() == Message.TYPE_IMAGE) {
565 if (message.getCounterpart() != null) {
566 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
567 if (!conv.hasValidOtrSession()) {
568 conv.startOtrSession(this, message.getCounterpart().getResourcepart(),
569 true);
570 message.setStatus(Message.STATUS_WAITING);
571 } else if (conv.hasValidOtrSession()
572 && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
573 mJingleConnectionManager
574 .createNewConnection(message);
575 }
576 } else {
577 mJingleConnectionManager.createNewConnection(message);
578 }
579 } else {
580 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
581 conv.startOtrIfNeeded();
582 }
583 message.setStatus(Message.STATUS_WAITING);
584 }
585 } else {
586 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
587 if (!conv.hasValidOtrSession() && (message.getCounterpart() != null)) {
588 conv.startOtrSession(this, message.getCounterpart().getResourcepart(), true);
589 message.setStatus(Message.STATUS_WAITING);
590 } else if (conv.hasValidOtrSession()) {
591 if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
592 packet = mMessageGenerator.generateOtrChat(message);
593 send = true;
594 } else {
595 message.setStatus(Message.STATUS_WAITING);
596 conv.startOtrIfNeeded();
597 }
598 } else {
599 message.setStatus(Message.STATUS_WAITING);
600 }
601 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
602 message.getConversation().endOtrIfNeeded();
603 failWaitingOtrMessages(message.getConversation());
604 packet = mMessageGenerator.generatePgpChat(message);
605 send = true;
606 } else {
607 message.getConversation().endOtrIfNeeded();
608 failWaitingOtrMessages(message.getConversation());
609 packet = mMessageGenerator.generateChat(message);
610 send = true;
611 }
612 }
613 if (!account.getXmppConnection().getFeatures().sm()
614 && conv.getMode() != Conversation.MODE_MULTI) {
615 message.setStatus(Message.STATUS_SEND);
616 }
617 } else {
618 message.setStatus(Message.STATUS_WAITING);
619 if (message.getType() == Message.TYPE_TEXT) {
620 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
621 String pgpBody = message.getEncryptedBody();
622 String decryptedBody = message.getBody();
623 message.setBody(pgpBody);
624 message.setEncryption(Message.ENCRYPTION_PGP);
625 databaseBackend.createMessage(message);
626 saveInDb = false;
627 message.setBody(decryptedBody);
628 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
629 } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
630 if (!conv.hasValidOtrSession()
631 && message.getCounterpart() != null) {
632 conv.startOtrSession(this, message.getCounterpart().getResourcepart(), false);
633 }
634 }
635 }
636
637 }
638 conv.add(message);
639 if (saveInDb) {
640 if (message.getEncryption() == Message.ENCRYPTION_NONE
641 || saveEncryptedMessages()) {
642 databaseBackend.createMessage(message);
643 }
644 }
645 if ((send) && (packet != null)) {
646 sendMessagePacket(account, packet);
647 }
648 updateConversationUi();
649 }
650
651 private void sendUnsendMessages(Conversation conversation) {
652 for (int i = 0; i < conversation.getMessages().size(); ++i) {
653 int status = conversation.getMessages().get(i).getStatus();
654 if (status == Message.STATUS_WAITING) {
655 resendMessage(conversation.getMessages().get(i));
656 }
657 }
658 }
659
660 private void resendMessage(Message message) {
661 Account account = message.getConversation().getAccount();
662 MessagePacket packet = null;
663 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
664 Presences presences = message.getConversation().getContact()
665 .getPresences();
666 if (!message.getConversation().hasValidOtrSession()) {
667 if ((message.getCounterpart() != null)
668 && (presences.has(message.getCounterpart().getResourcepart()))) {
669 message.getConversation().startOtrSession(this,
670 message.getCounterpart().getResourcepart(), true);
671 } else {
672 if (presences.size() == 1) {
673 String presence = presences.asStringArray()[0];
674 message.getConversation().startOtrSession(this,
675 presence, true);
676 }
677 }
678 } else {
679 if (message.getConversation().getOtrSession()
680 .getSessionStatus() == SessionStatus.ENCRYPTED) {
681 if (message.getType() == Message.TYPE_TEXT) {
682 packet = mMessageGenerator.generateOtrChat(message,
683 true);
684 } else if (message.getType() == Message.TYPE_IMAGE) {
685 mJingleConnectionManager.createNewConnection(message);
686 }
687 }
688 }
689 } else if (message.getType() == Message.TYPE_TEXT) {
690 if (message.getEncryption() == Message.ENCRYPTION_NONE) {
691 packet = mMessageGenerator.generateChat(message, true);
692 } else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
693 || (message.getEncryption() == Message.ENCRYPTION_PGP)) {
694 packet = mMessageGenerator.generatePgpChat(message, true);
695 }
696 } else if (message.getType() == Message.TYPE_IMAGE) {
697 Contact contact = message.getConversation().getContact();
698 Presences presences = contact.getPresences();
699 if ((message.getCounterpart() != null)
700 && (presences.has(message.getCounterpart().getResourcepart()))) {
701 markMessage(message, Message.STATUS_OFFERED);
702 mJingleConnectionManager.createNewConnection(message);
703 } else {
704 if (presences.size() == 1) {
705 String presence = presences.asStringArray()[0];
706 try {
707 message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence));
708 } catch (InvalidJidException e) {
709 return;
710 }
711 markMessage(message, Message.STATUS_OFFERED);
712 mJingleConnectionManager.createNewConnection(message);
713 }
714 }
715 }
716 if (packet != null) {
717 if (!account.getXmppConnection().getFeatures().sm()
718 && message.getConversation().getMode() != Conversation.MODE_MULTI) {
719 markMessage(message, Message.STATUS_SEND);
720 } else {
721 markMessage(message, Message.STATUS_UNSEND);
722 }
723 sendMessagePacket(account, packet);
724 }
725 }
726
727 public void fetchRosterFromServer(Account account) {
728 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
729 if (!"".equals(account.getRosterVersion())) {
730 Log.d(Config.LOGTAG, account.getJid().toBareJid()
731 + ": fetching roster version " + account.getRosterVersion());
732 } else {
733 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
734 }
735 iqPacket.query("jabber:iq:roster").setAttribute("ver",
736 account.getRosterVersion());
737 account.getXmppConnection().sendIqPacket(iqPacket,
738 new OnIqPacketReceived() {
739
740 @Override
741 public void onIqPacketReceived(final Account account,
742 IqPacket packet) {
743 Element query = packet.findChild("query");
744 if (query != null) {
745 account.getRoster().markAllAsNotInRoster();
746 mIqParser.rosterItems(account, query);
747 }
748 }
749 });
750 }
751
752 public void fetchBookmarks(Account account) {
753 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
754 Element query = iqPacket.query("jabber:iq:private");
755 query.addChild("storage", "storage:bookmarks");
756 OnIqPacketReceived callback = new OnIqPacketReceived() {
757
758 @Override
759 public void onIqPacketReceived(Account account, IqPacket packet) {
760 Element query = packet.query();
761 List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
762 Element storage = query.findChild("storage",
763 "storage:bookmarks");
764 if (storage != null) {
765 for (Element item : storage.getChildren()) {
766 if (item.getName().equals("conference")) {
767 Bookmark bookmark = Bookmark.parse(item, account);
768 bookmarks.add(bookmark);
769 Conversation conversation = find(bookmark);
770 if (conversation != null) {
771 conversation.setBookmark(bookmark);
772 } else {
773 if (bookmark.autojoin()) {
774 conversation = findOrCreateConversation(
775 account, bookmark.getJid(), true);
776 conversation.setBookmark(bookmark);
777 joinMuc(conversation);
778 }
779 }
780 }
781 }
782 }
783 account.setBookmarks(bookmarks);
784 }
785 };
786 sendIqPacket(account, iqPacket, callback);
787
788 }
789
790 public void pushBookmarks(Account account) {
791 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET);
792 Element query = iqPacket.query("jabber:iq:private");
793 Element storage = query.addChild("storage", "storage:bookmarks");
794 for (Bookmark bookmark : account.getBookmarks()) {
795 storage.addChild(bookmark);
796 }
797 sendIqPacket(account, iqPacket, null);
798 }
799
800 private void mergePhoneContactsWithRoster() {
801 PhoneHelper.loadPhoneContacts(getApplicationContext(),
802 new OnPhoneContactsLoadedListener() {
803 @Override
804 public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
805 for (Account account : accounts) {
806 account.getRoster().clearSystemAccounts();
807 }
808 for (Bundle phoneContact : phoneContacts) {
809 for (Account account : accounts) {
810 Jid jid;
811 try {
812 jid = Jid.fromString(phoneContact.getString("jid"));
813 } catch (final InvalidJidException e) {
814 // TODO: Warn if contact import fails here?
815 break;
816 }
817 final Contact contact = account.getRoster()
818 .getContact(jid);
819 String systemAccount = phoneContact
820 .getInt("phoneid")
821 + "#"
822 + phoneContact.getString("lookup");
823 contact.setSystemAccount(systemAccount);
824 contact.setPhotoUri(phoneContact
825 .getString("photouri"));
826 contact.setSystemName(phoneContact
827 .getString("displayname"));
828 getAvatarService().clear(contact);
829 }
830 }
831 }
832 });
833 }
834
835 public List<Conversation> getConversations() {
836 if (this.conversations == null) {
837 Hashtable<String, Account> accountLookupTable = new Hashtable<>();
838 for (Account account : this.accounts) {
839 accountLookupTable.put(account.getUuid(), account);
840 }
841 this.conversations = databaseBackend
842 .getConversations(Conversation.STATUS_AVAILABLE);
843 for (Conversation conv : this.conversations) {
844 Account account = accountLookupTable.get(conv.getAccountUuid());
845 conv.setAccount(account);
846 conv.setMessages(databaseBackend.getMessages(conv, 50));
847 checkDeletedFiles(conv);
848 }
849 }
850 return this.conversations;
851 }
852
853 private void checkDeletedFiles(Conversation conversation) {
854 for (Message message : conversation.getMessages()) {
855 if (message.getType() == Message.TYPE_IMAGE
856 && message.getEncryption() != Message.ENCRYPTION_PGP) {
857 if (!getFileBackend().isFileAvailable(message)) {
858 message.setDownloadable(new DeletedDownloadable());
859 }
860 }
861 }
862 }
863
864 private void markFileDeleted(String uuid) {
865 for (Conversation conversation : getConversations()) {
866 for (Message message : conversation.getMessages()) {
867 if (message.getType() == Message.TYPE_IMAGE
868 && message.getEncryption() != Message.ENCRYPTION_PGP
869 && message.getUuid().equals(uuid)) {
870 if (!getFileBackend().isFileAvailable(message)) {
871 message.setDownloadable(new DeletedDownloadable());
872 updateConversationUi();
873 }
874 return;
875 }
876 }
877 }
878 }
879
880 public void populateWithOrderedConversations(List<Conversation> list) {
881 populateWithOrderedConversations(list, true);
882 }
883
884 public void populateWithOrderedConversations(List<Conversation> list,
885 boolean includeConferences) {
886 list.clear();
887 if (includeConferences) {
888 list.addAll(getConversations());
889 } else {
890 for (Conversation conversation : getConversations()) {
891 if (conversation.getMode() == Conversation.MODE_SINGLE) {
892 list.add(conversation);
893 }
894 }
895 }
896 Collections.sort(list, new Comparator<Conversation>() {
897 @Override
898 public int compare(Conversation lhs, Conversation rhs) {
899 Message left = lhs.getLatestMessage();
900 Message right = rhs.getLatestMessage();
901 if (left.getTimeSent() > right.getTimeSent()) {
902 return -1;
903 } else if (left.getTimeSent() < right.getTimeSent()) {
904 return 1;
905 } else {
906 return 0;
907 }
908 }
909 });
910 }
911
912 public int loadMoreMessages(Conversation conversation, long timestamp) {
913 List<Message> messages = databaseBackend.getMessages(conversation, 50,
914 timestamp);
915 for (Message message : messages) {
916 message.setConversation(conversation);
917 }
918 conversation.addAll(0, messages);
919 return messages.size();
920 }
921
922 public List<Account> getAccounts() {
923 return this.accounts;
924 }
925
926 public Conversation find(List<Conversation> haystack, Contact contact) {
927 for (Conversation conversation : haystack) {
928 if (conversation.getContact() == contact) {
929 return conversation;
930 }
931 }
932 return null;
933 }
934
935 public Conversation find(final List<Conversation> haystack,
936 final Account account,
937 final Jid jid) {
938 for (Conversation conversation : haystack) {
939 if ((account == null || conversation.getAccount() == account)
940 && (conversation.getContactJid().toBareJid().equals(jid.toBareJid()))) {
941 return conversation;
942 }
943 }
944 return null;
945 }
946
947 public Conversation findOrCreateConversation(final Account account, final Jid jid,
948 final boolean muc) {
949 Conversation conversation = find(account, jid);
950 if (conversation != null) {
951 return conversation;
952 }
953 conversation = databaseBackend.findConversation(account, jid);
954 if (conversation != null) {
955 conversation.setStatus(Conversation.STATUS_AVAILABLE);
956 conversation.setAccount(account);
957 if (muc) {
958 conversation.setMode(Conversation.MODE_MULTI);
959 } else {
960 conversation.setMode(Conversation.MODE_SINGLE);
961 }
962 conversation.setMessages(databaseBackend.getMessages(conversation,
963 50));
964 this.databaseBackend.updateConversation(conversation);
965 } else {
966 String conversationName;
967 Contact contact = account.getRoster().getContact(jid);
968 if (contact != null) {
969 conversationName = contact.getDisplayName();
970 } else {
971 conversationName = jid.getLocalpart();
972 }
973 if (muc) {
974 conversation = new Conversation(conversationName, account, jid,
975 Conversation.MODE_MULTI);
976 } else {
977 conversation = new Conversation(conversationName, account, jid,
978 Conversation.MODE_SINGLE);
979 }
980 this.databaseBackend.createConversation(conversation);
981 }
982 this.conversations.add(conversation);
983 updateConversationUi();
984 return conversation;
985 }
986
987 public void archiveConversation(Conversation conversation) {
988 if (conversation.getMode() == Conversation.MODE_MULTI) {
989 if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
990 Bookmark bookmark = conversation.getBookmark();
991 if (bookmark != null && bookmark.autojoin()) {
992 bookmark.setAutojoin(false);
993 pushBookmarks(bookmark.getAccount());
994 }
995 }
996 leaveMuc(conversation);
997 } else {
998 conversation.endOtrIfNeeded();
999 }
1000 this.databaseBackend.updateConversation(conversation);
1001 this.conversations.remove(conversation);
1002 updateConversationUi();
1003 }
1004
1005 public void clearConversationHistory(Conversation conversation) {
1006 this.databaseBackend.deleteMessagesInConversation(conversation);
1007 conversation.getMessages().clear();
1008 updateConversationUi();
1009 }
1010
1011 public int getConversationCount() {
1012 return this.databaseBackend.getConversationCount();
1013 }
1014
1015 public void createAccount(Account account) {
1016 databaseBackend.createAccount(account);
1017 this.accounts.add(account);
1018 this.reconnectAccount(account, false);
1019 updateAccountUi();
1020 }
1021
1022 public void updateAccount(Account account) {
1023 this.statusListener.onStatusChanged(account);
1024 databaseBackend.updateAccount(account);
1025 reconnectAccount(account, false);
1026 updateAccountUi();
1027 UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1028 }
1029
1030 public void deleteAccount(Account account) {
1031 for (Conversation conversation : conversations) {
1032 if (conversation.getAccount() == account) {
1033 if (conversation.getMode() == Conversation.MODE_MULTI) {
1034 leaveMuc(conversation);
1035 } else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1036 conversation.endOtrIfNeeded();
1037 }
1038 conversations.remove(conversation);
1039 }
1040 }
1041 if (account.getXmppConnection() != null) {
1042 this.disconnect(account, true);
1043 }
1044 databaseBackend.deleteAccount(account);
1045 this.accounts.remove(account);
1046 updateAccountUi();
1047 UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1048 }
1049
1050 private void removeStaleListeners() {
1051 boolean removedListener = false;
1052 synchronized (this.convChangedListenerCount) {
1053 if (this.mOnConversationUpdate != null) {
1054 this.mOnConversationUpdate = null;
1055 this.convChangedListenerCount = 0;
1056 this.mNotificationService.setIsInForeground(false);
1057 removedListener = true;
1058 }
1059 }
1060 synchronized (this.accountChangedListenerCount) {
1061 if (this.mOnAccountUpdate != null) {
1062 this.mOnAccountUpdate = null;
1063 this.accountChangedListenerCount = 0;
1064 removedListener = true;
1065 }
1066 }
1067 synchronized (this.rosterChangedListenerCount) {
1068 if (this.mOnRosterUpdate != null) {
1069 this.mOnRosterUpdate = null;
1070 this.rosterChangedListenerCount = 0;
1071 removedListener = true;
1072 }
1073 }
1074 if (removedListener) {
1075 final String msg = "removed stale listeners";
1076 Log.d(Config.LOGTAG, msg);
1077 checkListeners();
1078 try {
1079 OutputStream os = openFileOutput("stacktrace.txt", MODE_PRIVATE);
1080 os.write(msg.getBytes());
1081 os.flush();
1082 os.close();
1083 } catch (final FileNotFoundException ignored) {
1084
1085 } catch (final IOException ignored) {
1086 }
1087 }
1088 }
1089
1090 public void setOnConversationListChangedListener(
1091 OnConversationUpdate listener) {
1092 /*if (!isScreenOn()) {
1093 Log.d(Config.LOGTAG,
1094 "ignoring setOnConversationListChangedListener");
1095 return;
1096 }*/
1097 synchronized (this.convChangedListenerCount) {
1098 if (checkListeners()) {
1099 switchToForeground();
1100 }
1101 this.mOnConversationUpdate = listener;
1102 this.mNotificationService.setIsInForeground(true);
1103 this.convChangedListenerCount++;
1104 }
1105 }
1106
1107 public void removeOnConversationListChangedListener() {
1108 synchronized (this.convChangedListenerCount) {
1109 this.convChangedListenerCount--;
1110 if (this.convChangedListenerCount <= 0) {
1111 this.convChangedListenerCount = 0;
1112 this.mOnConversationUpdate = null;
1113 this.mNotificationService.setIsInForeground(false);
1114 if (checkListeners()) {
1115 switchToBackground();
1116 }
1117 }
1118 }
1119 }
1120
1121 public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1122 /*if (!isScreenOn()) {
1123 Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener");
1124 return;
1125 }*/
1126 synchronized (this.accountChangedListenerCount) {
1127 if (checkListeners()) {
1128 switchToForeground();
1129 }
1130 this.mOnAccountUpdate = listener;
1131 this.accountChangedListenerCount++;
1132 }
1133 }
1134
1135 public void removeOnAccountListChangedListener() {
1136 synchronized (this.accountChangedListenerCount) {
1137 this.accountChangedListenerCount--;
1138 if (this.accountChangedListenerCount <= 0) {
1139 this.mOnAccountUpdate = null;
1140 this.accountChangedListenerCount = 0;
1141 if (checkListeners()) {
1142 switchToBackground();
1143 }
1144 }
1145 }
1146 }
1147
1148 public void setOnRosterUpdateListener(OnRosterUpdate listener) {
1149 /*if (!isScreenOn()) {
1150 Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener");
1151 return;
1152 }*/
1153 synchronized (this.rosterChangedListenerCount) {
1154 if (checkListeners()) {
1155 switchToForeground();
1156 }
1157 this.mOnRosterUpdate = listener;
1158 this.rosterChangedListenerCount++;
1159 }
1160 }
1161
1162 public void removeOnRosterUpdateListener() {
1163 synchronized (this.rosterChangedListenerCount) {
1164 this.rosterChangedListenerCount--;
1165 if (this.rosterChangedListenerCount <= 0) {
1166 this.rosterChangedListenerCount = 0;
1167 this.mOnRosterUpdate = null;
1168 if (checkListeners()) {
1169 switchToBackground();
1170 }
1171 }
1172 }
1173 }
1174
1175 private boolean checkListeners() {
1176 return (this.mOnAccountUpdate == null
1177 && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null);
1178 }
1179
1180 private void switchToForeground() {
1181 for (Account account : getAccounts()) {
1182 if (account.getStatus() == Account.STATUS_ONLINE) {
1183 XmppConnection connection = account.getXmppConnection();
1184 if (connection != null && connection.getFeatures().csi()) {
1185 connection.sendActive();
1186 }
1187 }
1188 }
1189 Log.d(Config.LOGTAG, "app switched into foreground");
1190 }
1191
1192 private void switchToBackground() {
1193 for (Account account : getAccounts()) {
1194 if (account.getStatus() == Account.STATUS_ONLINE) {
1195 XmppConnection connection = account.getXmppConnection();
1196 if (connection != null && connection.getFeatures().csi()) {
1197 connection.sendInactive();
1198 }
1199 }
1200 }
1201 this.mNotificationService.setIsInForeground(false);
1202 Log.d(Config.LOGTAG, "app switched into background");
1203 }
1204
1205 private boolean isScreenOn() {
1206 PowerManager pm = (PowerManager) this
1207 .getSystemService(Context.POWER_SERVICE);
1208 return pm.isScreenOn();
1209 }
1210
1211 public void connectMultiModeConversations(Account account) {
1212 List<Conversation> conversations = getConversations();
1213 for (Conversation conversation : conversations) {
1214 if ((conversation.getMode() == Conversation.MODE_MULTI)
1215 && (conversation.getAccount() == account)) {
1216 joinMuc(conversation);
1217 }
1218 }
1219 }
1220
1221 public void joinMuc(Conversation conversation) {
1222 Account account = conversation.getAccount();
1223 account.pendingConferenceJoins.remove(conversation);
1224 account.pendingConferenceLeaves.remove(conversation);
1225 if (account.getStatus() == Account.STATUS_ONLINE) {
1226 Log.d(Config.LOGTAG,
1227 "joining conversation " + conversation.getContactJid());
1228 String nick = conversation.getMucOptions().getProposedNick();
1229 conversation.getMucOptions().setJoinNick(nick);
1230 PresencePacket packet = new PresencePacket();
1231 final Jid joinJid = conversation.getMucOptions().getJoinJid();
1232 packet.setTo(conversation.getMucOptions().getJoinJid());
1233 Element x = new Element("x");
1234 x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
1235 if (conversation.getMucOptions().getPassword() != null) {
1236 Element password = x.addChild("password");
1237 password.setContent(conversation.getMucOptions().getPassword());
1238 }
1239 String sig = account.getPgpSignature();
1240 if (sig != null) {
1241 packet.addChild("status").setContent("online");
1242 packet.addChild("x", "jabber:x:signed").setContent(sig);
1243 }
1244 if (conversation.getMessages().size() != 0) {
1245 final SimpleDateFormat mDateFormat = new SimpleDateFormat(
1246 "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
1247 mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
1248 Date date = new Date(conversation.getLatestMessage()
1249 .getTimeSent() + 1000);
1250 x.addChild("history").setAttribute("since",
1251 mDateFormat.format(date));
1252 }
1253 packet.addChild(x);
1254 sendPresencePacket(account, packet);
1255 if (!joinJid.equals(conversation.getContactJid())) {
1256 conversation.setContactJid(joinJid);
1257 databaseBackend.updateConversation(conversation);
1258 }
1259 } else {
1260 account.pendingConferenceJoins.add(conversation);
1261 }
1262 }
1263
1264 public void setOnRenameListener(OnRenameListener listener) {
1265 this.renameListener = listener;
1266 }
1267
1268 public void providePasswordForMuc(Conversation conversation, String password) {
1269 if (conversation.getMode() == Conversation.MODE_MULTI) {
1270 conversation.getMucOptions().setPassword(password);
1271 if (conversation.getBookmark() != null) {
1272 conversation.getBookmark().setAutojoin(true);
1273 pushBookmarks(conversation.getAccount());
1274 }
1275 databaseBackend.updateConversation(conversation);
1276 joinMuc(conversation);
1277 }
1278 }
1279
1280 public void renameInMuc(final Conversation conversation, final String nick) {
1281 final MucOptions options = conversation.getMucOptions();
1282 options.setJoinNick(nick);
1283 if (options.online()) {
1284 Account account = conversation.getAccount();
1285 options.setOnRenameListener(new OnRenameListener() {
1286
1287 @Override
1288 public void onRename(boolean success) {
1289 if (renameListener != null) {
1290 renameListener.onRename(success);
1291 }
1292 if (success) {
1293 conversation.setContactJid(conversation.getMucOptions()
1294 .getJoinJid());
1295 databaseBackend.updateConversation(conversation);
1296 Bookmark bookmark = conversation.getBookmark();
1297 if (bookmark != null) {
1298 bookmark.setNick(nick);
1299 pushBookmarks(bookmark.getAccount());
1300 }
1301 }
1302 }
1303 });
1304 options.flagAboutToRename();
1305 PresencePacket packet = new PresencePacket();
1306 packet.setTo(options.getJoinJid());
1307 packet.setFrom(conversation.getAccount().getJid());
1308
1309 String sig = account.getPgpSignature();
1310 if (sig != null) {
1311 packet.addChild("status").setContent("online");
1312 packet.addChild("x", "jabber:x:signed").setContent(sig);
1313 }
1314 sendPresencePacket(account, packet);
1315 } else {
1316 conversation.setContactJid(options.getJoinJid());
1317 databaseBackend.updateConversation(conversation);
1318 if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1319 Bookmark bookmark = conversation.getBookmark();
1320 if (bookmark != null) {
1321 bookmark.setNick(nick);
1322 pushBookmarks(bookmark.getAccount());
1323 }
1324 joinMuc(conversation);
1325 }
1326 }
1327 }
1328
1329 public void leaveMuc(Conversation conversation) {
1330 Account account = conversation.getAccount();
1331 account.pendingConferenceJoins.remove(conversation);
1332 account.pendingConferenceLeaves.remove(conversation);
1333 if (account.getStatus() == Account.STATUS_ONLINE) {
1334 PresencePacket packet = new PresencePacket();
1335 packet.setTo(conversation.getMucOptions().getJoinJid());
1336 packet.setFrom(conversation.getAccount().getJid());
1337 packet.setAttribute("type", "unavailable");
1338 sendPresencePacket(conversation.getAccount(), packet);
1339 conversation.getMucOptions().setOffline();
1340 conversation.deregisterWithBookmark();
1341 Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1342 + ": leaving muc " + conversation.getContactJid());
1343 } else {
1344 account.pendingConferenceLeaves.add(conversation);
1345 }
1346 }
1347
1348 public void disconnect(Account account, boolean force) {
1349 if ((account.getStatus() == Account.STATUS_ONLINE)
1350 || (account.getStatus() == Account.STATUS_DISABLED)) {
1351 if (!force) {
1352 List<Conversation> conversations = getConversations();
1353 for (Conversation conversation : conversations) {
1354 if (conversation.getAccount() == account) {
1355 if (conversation.getMode() == Conversation.MODE_MULTI) {
1356 leaveMuc(conversation);
1357 } else {
1358 if (conversation.endOtrIfNeeded()) {
1359 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1360 + ": ended otr session with "
1361 + conversation.getContactJid());
1362 }
1363 }
1364 }
1365 }
1366 }
1367 account.getXmppConnection().disconnect(force);
1368 }
1369 }
1370
1371 @Override
1372 public IBinder onBind(Intent intent) {
1373 return mBinder;
1374 }
1375
1376 public void updateMessage(Message message) {
1377 databaseBackend.updateMessage(message);
1378 updateConversationUi();
1379 }
1380
1381 protected void syncDirtyContacts(Account account) {
1382 for (Contact contact : account.getRoster().getContacts()) {
1383 if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1384 pushContactToServer(contact);
1385 }
1386 if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1387 deleteContactOnServer(contact);
1388 }
1389 }
1390 }
1391
1392 public void createContact(Contact contact) {
1393 SharedPreferences sharedPref = getPreferences();
1394 boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1395 if (autoGrant) {
1396 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1397 contact.setOption(Contact.Options.ASKING);
1398 }
1399 pushContactToServer(contact);
1400 }
1401
1402 public void onOtrSessionEstablished(Conversation conversation) {
1403 Account account = conversation.getAccount();
1404 List<Message> messages = conversation.getMessages();
1405 Session otrSession = conversation.getOtrSession();
1406 Log.d(Config.LOGTAG,
1407 account.getJid().toBareJid() + " otr session established with "
1408 + conversation.getContactJid() + "/"
1409 + otrSession.getSessionID().getUserID());
1410 for (Message msg : messages) {
1411 if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
1412 && (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
1413 SessionID id = otrSession.getSessionID();
1414 try {
1415 msg.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
1416 } catch (InvalidJidException e) {
1417 break;
1418 }
1419 if (msg.getType() == Message.TYPE_TEXT) {
1420 MessagePacket outPacket = mMessageGenerator
1421 .generateOtrChat(msg, true);
1422 if (outPacket != null) {
1423 msg.setStatus(Message.STATUS_SEND);
1424 databaseBackend.updateMessage(msg);
1425 sendMessagePacket(account, outPacket);
1426 }
1427 } else if (msg.getType() == Message.TYPE_IMAGE) {
1428 mJingleConnectionManager.createNewConnection(msg);
1429 }
1430 }
1431 }
1432 updateConversationUi();
1433 }
1434
1435 public boolean renewSymmetricKey(Conversation conversation) {
1436 Account account = conversation.getAccount();
1437 byte[] symmetricKey = new byte[32];
1438 this.mRandom.nextBytes(symmetricKey);
1439 Session otrSession = conversation.getOtrSession();
1440 if (otrSession != null) {
1441 MessagePacket packet = new MessagePacket();
1442 packet.setType(MessagePacket.TYPE_CHAT);
1443 packet.setFrom(account.getJid());
1444 packet.addChild("private", "urn:xmpp:carbons:2");
1445 packet.addChild("no-copy", "urn:xmpp:hints");
1446 packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
1447 + otrSession.getSessionID().getUserID());
1448 try {
1449 packet.setBody(otrSession
1450 .transformSending(CryptoHelper.FILETRANSFER
1451 + CryptoHelper.bytesToHex(symmetricKey)));
1452 sendMessagePacket(account, packet);
1453 conversation.setSymmetricKey(symmetricKey);
1454 return true;
1455 } catch (OtrException e) {
1456 return false;
1457 }
1458 }
1459 return false;
1460 }
1461
1462 public void pushContactToServer(Contact contact) {
1463 contact.resetOption(Contact.Options.DIRTY_DELETE);
1464 contact.setOption(Contact.Options.DIRTY_PUSH);
1465 Account account = contact.getAccount();
1466 if (account.getStatus() == Account.STATUS_ONLINE) {
1467 boolean ask = contact.getOption(Contact.Options.ASKING);
1468 boolean sendUpdates = contact
1469 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
1470 && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
1471 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1472 iq.query("jabber:iq:roster").addChild(contact.asElement());
1473 account.getXmppConnection().sendIqPacket(iq, null);
1474 if (sendUpdates) {
1475 sendPresencePacket(account,
1476 mPresenceGenerator.sendPresenceUpdatesTo(contact));
1477 }
1478 if (ask) {
1479 sendPresencePacket(account,
1480 mPresenceGenerator.requestPresenceUpdatesFrom(contact));
1481 }
1482 }
1483 }
1484
1485 public void publishAvatar(Account account, Uri image,
1486 final UiCallback<Avatar> callback) {
1487 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
1488 final int size = Config.AVATAR_SIZE;
1489 final Avatar avatar = getFileBackend()
1490 .getPepAvatar(image, size, format);
1491 if (avatar != null) {
1492 avatar.height = size;
1493 avatar.width = size;
1494 if (format.equals(Bitmap.CompressFormat.WEBP)) {
1495 avatar.type = "image/webp";
1496 } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
1497 avatar.type = "image/jpeg";
1498 } else if (format.equals(Bitmap.CompressFormat.PNG)) {
1499 avatar.type = "image/png";
1500 }
1501 if (!getFileBackend().save(avatar)) {
1502 callback.error(R.string.error_saving_avatar, avatar);
1503 return;
1504 }
1505 IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
1506 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1507
1508 @Override
1509 public void onIqPacketReceived(Account account, IqPacket result) {
1510 if (result.getType() == IqPacket.TYPE_RESULT) {
1511 IqPacket packet = XmppConnectionService.this.mIqGenerator
1512 .publishAvatarMetadata(avatar);
1513 sendIqPacket(account, packet, new OnIqPacketReceived() {
1514
1515 @Override
1516 public void onIqPacketReceived(Account account,
1517 IqPacket result) {
1518 if (result.getType() == IqPacket.TYPE_RESULT) {
1519 if (account.setAvatar(avatar.getFilename())) {
1520 databaseBackend.updateAccount(account);
1521 }
1522 callback.success(avatar);
1523 } else {
1524 callback.error(
1525 R.string.error_publish_avatar_server_reject,
1526 avatar);
1527 }
1528 }
1529 });
1530 } else {
1531 callback.error(
1532 R.string.error_publish_avatar_server_reject,
1533 avatar);
1534 }
1535 }
1536 });
1537 } else {
1538 callback.error(R.string.error_publish_avatar_converting, null);
1539 }
1540 }
1541
1542 public void fetchAvatar(Account account, Avatar avatar) {
1543 fetchAvatar(account, avatar, null);
1544 }
1545
1546 public void fetchAvatar(Account account, final Avatar avatar,
1547 final UiCallback<Avatar> callback) {
1548 IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
1549 sendIqPacket(account, packet, new OnIqPacketReceived() {
1550
1551 @Override
1552 public void onIqPacketReceived(Account account, IqPacket result) {
1553 final String ERROR = account.getJid().toBareJid()
1554 + ": fetching avatar for " + avatar.owner + " failed ";
1555 if (result.getType() == IqPacket.TYPE_RESULT) {
1556 avatar.image = mIqParser.avatarData(result);
1557 if (avatar.image != null) {
1558 if (getFileBackend().save(avatar)) {
1559 if (account.getJid().toBareJid().equals(avatar.owner)) {
1560 if (account.setAvatar(avatar.getFilename())) {
1561 databaseBackend.updateAccount(account);
1562 }
1563 getAvatarService().clear(account);
1564 updateConversationUi();
1565 updateAccountUi();
1566 } else {
1567 Contact contact = account.getRoster()
1568 .getContact(avatar.owner);
1569 contact.setAvatar(avatar.getFilename());
1570 getAvatarService().clear(contact);
1571 updateConversationUi();
1572 updateRosterUi();
1573 }
1574 if (callback != null) {
1575 callback.success(avatar);
1576 }
1577 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1578 + ": succesfully fetched avatar for "
1579 + avatar.owner);
1580 return;
1581 }
1582 } else {
1583
1584 Log.d(Config.LOGTAG, ERROR + "(parsing error)");
1585 }
1586 } else {
1587 Element error = result.findChild("error");
1588 if (error == null) {
1589 Log.d(Config.LOGTAG, ERROR + "(server error)");
1590 } else {
1591 Log.d(Config.LOGTAG, ERROR + error.toString());
1592 }
1593 }
1594 if (callback != null) {
1595 callback.error(0, null);
1596 }
1597
1598 }
1599 });
1600 }
1601
1602 public void checkForAvatar(Account account,
1603 final UiCallback<Avatar> callback) {
1604 IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
1605 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1606
1607 @Override
1608 public void onIqPacketReceived(Account account, IqPacket packet) {
1609 if (packet.getType() == IqPacket.TYPE_RESULT) {
1610 Element pubsub = packet.findChild("pubsub",
1611 "http://jabber.org/protocol/pubsub");
1612 if (pubsub != null) {
1613 Element items = pubsub.findChild("items");
1614 if (items != null) {
1615 Avatar avatar = Avatar.parseMetadata(items);
1616 if (avatar != null) {
1617 avatar.owner = account.getJid().toBareJid();
1618 if (fileBackend.isAvatarCached(avatar)) {
1619 if (account.setAvatar(avatar.getFilename())) {
1620 databaseBackend.updateAccount(account);
1621 }
1622 getAvatarService().clear(account);
1623 callback.success(avatar);
1624 } else {
1625 fetchAvatar(account, avatar, callback);
1626 }
1627 return;
1628 }
1629 }
1630 }
1631 }
1632 callback.error(0, null);
1633 }
1634 });
1635 }
1636
1637 public void deleteContactOnServer(Contact contact) {
1638 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
1639 contact.resetOption(Contact.Options.DIRTY_PUSH);
1640 contact.setOption(Contact.Options.DIRTY_DELETE);
1641 Account account = contact.getAccount();
1642 if (account.getStatus() == Account.STATUS_ONLINE) {
1643 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1644 Element item = iq.query("jabber:iq:roster").addChild("item");
1645 item.setAttribute("jid", contact.getJid().toString());
1646 item.setAttribute("subscription", "remove");
1647 account.getXmppConnection().sendIqPacket(iq, null);
1648 }
1649 }
1650
1651 public void updateConversation(Conversation conversation) {
1652 this.databaseBackend.updateConversation(conversation);
1653 }
1654
1655 public void reconnectAccount(final Account account, final boolean force) {
1656 new Thread(new Runnable() {
1657
1658 @Override
1659 public void run() {
1660 if (account.getXmppConnection() != null) {
1661 disconnect(account, force);
1662 }
1663 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1664 if (account.getXmppConnection() == null) {
1665 account.setXmppConnection(createConnection(account));
1666 }
1667 Thread thread = new Thread(account.getXmppConnection());
1668 thread.start();
1669 scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2),
1670 false);
1671 } else {
1672 account.getRoster().clearPresences();
1673 account.setXmppConnection(null);
1674 }
1675 }
1676 }).start();
1677 }
1678
1679 public void invite(Conversation conversation, String contact) {
1680 MessagePacket packet = mMessageGenerator.invite(conversation, contact);
1681 sendMessagePacket(conversation.getAccount(), packet);
1682 }
1683
1684 public void resetSendingToWaiting(Account account) {
1685 for (Conversation conversation : getConversations()) {
1686 if (conversation.getAccount() == account) {
1687 for (Message message : conversation.getMessages()) {
1688 if (message.getType() != Message.TYPE_IMAGE
1689 && message.getStatus() == Message.STATUS_UNSEND) {
1690 markMessage(message, Message.STATUS_WAITING);
1691 }
1692 }
1693 }
1694 }
1695 }
1696
1697 public boolean markMessage(final Account account, final Jid recipient, final String uuid,
1698 final int status) {
1699 if (uuid == null) {
1700 return false;
1701 } else {
1702 for (Conversation conversation : getConversations()) {
1703 if (conversation.getContactJid().equals(recipient)
1704 && conversation.getAccount().equals(account)) {
1705 return markMessage(conversation, uuid, status);
1706 }
1707 }
1708 return false;
1709 }
1710 }
1711
1712 public boolean markMessage(Conversation conversation, String uuid,
1713 int status) {
1714 if (uuid == null) {
1715 return false;
1716 } else {
1717 for (Message message : conversation.getMessages()) {
1718 if (uuid.equals(message.getUuid())
1719 || (message.getStatus() >= Message.STATUS_SEND && uuid
1720 .equals(message.getRemoteMsgId()))) {
1721 markMessage(message, status);
1722 return true;
1723 }
1724 }
1725 return false;
1726 }
1727 }
1728
1729 public void markMessage(Message message, int status) {
1730 if (status == Message.STATUS_SEND_FAILED
1731 && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
1732 .getStatus() == Message.STATUS_SEND_DISPLAYED)) {
1733 return;
1734 }
1735 message.setStatus(status);
1736 databaseBackend.updateMessage(message);
1737 updateConversationUi();
1738 }
1739
1740 public SharedPreferences getPreferences() {
1741 return PreferenceManager
1742 .getDefaultSharedPreferences(getApplicationContext());
1743 }
1744
1745 public boolean forceEncryption() {
1746 return getPreferences().getBoolean("force_encryption", false);
1747 }
1748
1749 public boolean confirmMessages() {
1750 return getPreferences().getBoolean("confirm_messages", true);
1751 }
1752
1753 public boolean saveEncryptedMessages() {
1754 return !getPreferences().getBoolean("dont_save_encrypted", false);
1755 }
1756
1757 public boolean indicateReceived() {
1758 return getPreferences().getBoolean("indicate_received", false);
1759 }
1760
1761 public void updateConversationUi() {
1762 if (mOnConversationUpdate != null) {
1763 mOnConversationUpdate.onConversationUpdate();
1764 }
1765 }
1766
1767 public void updateAccountUi() {
1768 if (mOnAccountUpdate != null) {
1769 mOnAccountUpdate.onAccountUpdate();
1770 }
1771 }
1772
1773 public void updateRosterUi() {
1774 if (mOnRosterUpdate != null) {
1775 mOnRosterUpdate.onRosterUpdate();
1776 }
1777 }
1778
1779 public Account findAccountByJid(final Jid accountJid) {
1780 for (Account account : this.accounts) {
1781 if (account.getJid().toBareJid().equals(accountJid)) {
1782 return account;
1783 }
1784 }
1785 return null;
1786 }
1787
1788 public Conversation findConversationByUuid(String uuid) {
1789 for (Conversation conversation : getConversations()) {
1790 if (conversation.getUuid().equals(uuid)) {
1791 return conversation;
1792 }
1793 }
1794 return null;
1795 }
1796
1797 public void markRead(Conversation conversation, boolean calledByUi) {
1798 mNotificationService.clear(conversation);
1799 String id = conversation.getLatestMarkableMessageId();
1800 conversation.markRead();
1801 if (confirmMessages() && id != null && calledByUi) {
1802 Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1803 + ": sending read marker for " + conversation.getName());
1804 Account account = conversation.getAccount();
1805 final Jid to = conversation.getContactJid();
1806 this.sendMessagePacket(conversation.getAccount(),
1807 mMessageGenerator.confirm(account, to, id));
1808 }
1809 if (!calledByUi) {
1810 updateConversationUi();
1811 }
1812 }
1813
1814 public void failWaitingOtrMessages(Conversation conversation) {
1815 for (Message message : conversation.getMessages()) {
1816 if (message.getEncryption() == Message.ENCRYPTION_OTR
1817 && message.getStatus() == Message.STATUS_WAITING) {
1818 markMessage(message, Message.STATUS_SEND_FAILED);
1819 }
1820 }
1821 }
1822
1823 public SecureRandom getRNG() {
1824 return this.mRandom;
1825 }
1826
1827 public MemorizingTrustManager getMemorizingTrustManager() {
1828 return this.mMemorizingTrustManager;
1829 }
1830
1831 public PowerManager getPowerManager() {
1832 return this.pm;
1833 }
1834
1835 public LruCache<String, Bitmap> getBitmapCache() {
1836 return this.mBitmapCache;
1837 }
1838
1839 public void replyWithNotAcceptable(Account account, MessagePacket packet) {
1840 if (account.getStatus() == Account.STATUS_ONLINE) {
1841 MessagePacket error = this.mMessageGenerator
1842 .generateNotAcceptable(packet);
1843 sendMessagePacket(account, error);
1844 }
1845 }
1846
1847 public void syncRosterToDisk(final Account account) {
1848 new Thread(new Runnable() {
1849
1850 @Override
1851 public void run() {
1852 databaseBackend.writeRoster(account.getRoster());
1853 }
1854 }).start();
1855
1856 }
1857
1858 public List<String> getKnownHosts() {
1859 List<String> hosts = new ArrayList<>();
1860 for (Account account : getAccounts()) {
1861 if (!hosts.contains(account.getServer().toString())) {
1862 hosts.add(account.getServer().toString());
1863 }
1864 for (Contact contact : account.getRoster().getContacts()) {
1865 if (contact.showInRoster()) {
1866 final String server = contact.getServer().toString();
1867 if (server != null && !hosts.contains(server)) {
1868 hosts.add(server);
1869 }
1870 }
1871 }
1872 }
1873 return hosts;
1874 }
1875
1876 public List<String> getKnownConferenceHosts() {
1877 ArrayList<String> mucServers = new ArrayList<>();
1878 for (Account account : accounts) {
1879 if (account.getXmppConnection() != null) {
1880 String server = account.getXmppConnection().getMucServer();
1881 if (server != null && !mucServers.contains(server)) {
1882 mucServers.add(server);
1883 }
1884 }
1885 }
1886 return mucServers;
1887 }
1888
1889 public void sendMessagePacket(Account account, MessagePacket packet) {
1890 XmppConnection connection = account.getXmppConnection();
1891 if (connection != null) {
1892 connection.sendMessagePacket(packet);
1893 }
1894 }
1895
1896 public void sendPresencePacket(Account account, PresencePacket packet) {
1897 XmppConnection connection = account.getXmppConnection();
1898 if (connection != null) {
1899 connection.sendPresencePacket(packet);
1900 }
1901 }
1902
1903 public void sendIqPacket(Account account, IqPacket packet,
1904 OnIqPacketReceived callback) {
1905 XmppConnection connection = account.getXmppConnection();
1906 if (connection != null) {
1907 connection.sendIqPacket(packet, callback);
1908 }
1909 }
1910
1911 public MessageGenerator getMessageGenerator() {
1912 return this.mMessageGenerator;
1913 }
1914
1915 public PresenceGenerator getPresenceGenerator() {
1916 return this.mPresenceGenerator;
1917 }
1918
1919 public IqGenerator getIqGenerator() {
1920 return this.mIqGenerator;
1921 }
1922
1923 public JingleConnectionManager getJingleConnectionManager() {
1924 return this.mJingleConnectionManager;
1925 }
1926
1927 public List<Contact> findContacts(String jid) {
1928 ArrayList<Contact> contacts = new ArrayList<>();
1929 for (Account account : getAccounts()) {
1930 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1931 Contact contact = account.getRoster().getContactFromRoster(jid);
1932 if (contact != null) {
1933 contacts.add(contact);
1934 }
1935 }
1936 }
1937 return contacts;
1938 }
1939
1940 public NotificationService getNotificationService() {
1941 return this.mNotificationService;
1942 }
1943
1944 public HttpConnectionManager getHttpConnectionManager() {
1945 return this.mHttpConnectionManager;
1946 }
1947
1948 public void resendFailedMessages(Message message) {
1949 List<Message> messages = new ArrayList<>();
1950 Message current = message;
1951 while (current.getStatus() == Message.STATUS_SEND_FAILED) {
1952 messages.add(current);
1953 if (current.mergeable(current.next())) {
1954 current = current.next();
1955 } else {
1956 break;
1957 }
1958 }
1959 for (Message msg : messages) {
1960 markMessage(msg, Message.STATUS_WAITING);
1961 this.resendMessage(msg);
1962 }
1963 }
1964
1965 public interface OnConversationUpdate {
1966 public void onConversationUpdate();
1967 }
1968
1969 public interface OnAccountUpdate {
1970 public void onAccountUpdate();
1971 }
1972
1973 public interface OnRosterUpdate {
1974 public void onRosterUpdate();
1975 }
1976
1977 public class XmppConnectionBinder extends Binder {
1978 public XmppConnectionService getService() {
1979 return XmppConnectionService.this;
1980 }
1981 }
1982
1983 private class DeletedDownloadable implements Downloadable {
1984
1985 @Override
1986 public boolean start() {
1987 return false;
1988 }
1989
1990 @Override
1991 public int getStatus() {
1992 return Downloadable.STATUS_DELETED;
1993 }
1994
1995 @Override
1996 public long getFileSize() {
1997 return 0;
1998 }
1999
2000 }
2001}