aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/de/thedevstack/conversationsplus/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/thedevstack/conversationsplus/services')
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java23
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java295
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java24
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java371
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java557
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java2434
6 files changed, 3704 insertions, 0 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java
new file mode 100644
index 00000000..0dedc01d
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java
@@ -0,0 +1,23 @@
+package de.thedevstack.conversationsplus.services;
+
+public class AbstractConnectionManager {
+ protected XmppConnectionService mXmppConnectionService;
+
+ public AbstractConnectionManager(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public XmppConnectionService getXmppConnectionService() {
+ return this.mXmppConnectionService;
+ }
+
+ public long getAutoAcceptFileSize() {
+ String config = this.mXmppConnectionService.getPreferences().getString(
+ "auto_accept_file_size", "524288");
+ try {
+ return Long.parseLong(config);
+ } catch (NumberFormatException e) {
+ return 524288;
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java b/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java
new file mode 100644
index 00000000..7321fc08
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java
@@ -0,0 +1,295 @@
+package de.thedevstack.conversationsplus.services;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import de.thedevstack.conversationsplus.entities.Account;
+import de.thedevstack.conversationsplus.entities.Bookmark;
+import de.thedevstack.conversationsplus.entities.Contact;
+import de.thedevstack.conversationsplus.entities.Conversation;
+import de.thedevstack.conversationsplus.entities.ListItem;
+import de.thedevstack.conversationsplus.entities.MucOptions;
+import de.thedevstack.conversationsplus.utils.UIHelper;
+
+public class AvatarService {
+
+ private static final int FG_COLOR = 0xFFFAFAFA;
+ private static final int TRANSPARENT = 0x00000000;
+ private static final int PLACEHOLDER_COLOR = 0xFF202020;
+
+ private static final String PREFIX_CONTACT = "contact";
+ private static final String PREFIX_CONVERSATION = "conversation";
+ private static final String PREFIX_ACCOUNT = "account";
+ private static final String PREFIX_GENERIC = "generic";
+
+ final private ArrayList<Integer> sizes = new ArrayList<>();
+
+ protected XmppConnectionService mXmppConnectionService = null;
+
+ public AvatarService(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
+ final String KEY = key(contact, size);
+ Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
+ if (avatar != null || cachedOnly) {
+ return avatar;
+ }
+ if (contact.getProfilePhoto() != null) {
+ avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
+ }
+ if (avatar == null && contact.getAvatar() != null) {
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size);
+ }
+ if (avatar == null) {
+ avatar = get(contact.getDisplayName(), size, cachedOnly);
+ }
+ this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
+ return avatar;
+ }
+
+ public void clear(Contact contact) {
+ synchronized (this.sizes) {
+ for (Integer size : sizes) {
+ this.mXmppConnectionService.getBitmapCache().remove(
+ key(contact, size));
+ }
+ }
+ }
+
+ private String key(Contact contact, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_CONTACT + "_" + contact.getAccount().getJid().toBareJid() + "_"
+ + contact.getJid() + "_" + String.valueOf(size);
+ }
+
+ public Bitmap get(ListItem item, int size) {
+ return get(item,size,false);
+ }
+
+ public Bitmap get(ListItem item, int size, boolean cachedOnly) {
+ if (item instanceof Contact) {
+ return get((Contact) item, size,cachedOnly);
+ } else if (item instanceof Bookmark) {
+ Bookmark bookmark = (Bookmark) item;
+ if (bookmark.getConversation() != null) {
+ return get(bookmark.getConversation(), size, cachedOnly);
+ } else {
+ return get(bookmark.getDisplayName(), size, cachedOnly);
+ }
+ } else {
+ return get(item.getDisplayName(), size, cachedOnly);
+ }
+ }
+
+ public Bitmap get(Conversation conversation, int size) {
+ return get(conversation,size,false);
+ }
+
+ public Bitmap get(Conversation conversation, int size, boolean cachedOnly) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ return get(conversation.getContact(), size, cachedOnly);
+ } else {
+ return get(conversation.getMucOptions(), size, cachedOnly);
+ }
+ }
+
+ public void clear(Conversation conversation) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ clear(conversation.getContact());
+ } else {
+ clear(conversation.getMucOptions());
+ }
+ }
+
+ private Bitmap get(MucOptions mucOptions, int size, boolean cachedOnly) {
+ final String KEY = key(mucOptions, size);
+ Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
+ if (bitmap != null || cachedOnly) {
+ return bitmap;
+ }
+ final List<MucOptions.User> users = new ArrayList<>(mucOptions.getUsers());
+ int count = users.size();
+ bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ bitmap.eraseColor(TRANSPARENT);
+
+ if (count == 0) {
+ String name = mucOptions.getConversation().getName();
+ final String letter = name.isEmpty() ? "X" : name.substring(0,1);
+ final int color = UIHelper.getColorForName(name);
+ drawTile(canvas, letter, color, 0, 0, size, size);
+ } else if (count == 1) {
+ drawTile(canvas, users.get(0), 0, 0, size, size);
+ } else if (count == 2) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
+ drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size);
+ } else if (count == 3) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
+ drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1);
+ drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size,
+ size);
+ } else if (count == 4) {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
+ drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
+ drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
+ drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size,
+ size);
+ } else {
+ drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
+ drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
+ drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
+ drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1,
+ size, size);
+ }
+ this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+ return bitmap;
+ }
+
+ public void clear(MucOptions options) {
+ synchronized (this.sizes) {
+ for (Integer size : sizes) {
+ this.mXmppConnectionService.getBitmapCache().remove(
+ key(options, size));
+ }
+ }
+ }
+
+ private String key(MucOptions options, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid()
+ + "_" + String.valueOf(size);
+ }
+
+ public Bitmap get(Account account, int size) {
+ final String KEY = key(account, size);
+ Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY);
+ if (avatar != null) {
+ return avatar;
+ }
+ avatar = mXmppConnectionService.getFileBackend().getAvatar(
+ account.getAvatar(), size);
+ if (avatar == null) {
+ avatar = get(account.getJid().toBareJid().toString(), size,false);
+ }
+ mXmppConnectionService.getBitmapCache().put(KEY, avatar);
+ return avatar;
+ }
+
+ public void clear(Account account) {
+ synchronized (this.sizes) {
+ for (Integer size : sizes) {
+ this.mXmppConnectionService.getBitmapCache().remove(
+ key(account, size));
+ }
+ }
+ }
+
+ private String key(Account account, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_ACCOUNT + "_" + account.getUuid() + "_"
+ + String.valueOf(size);
+ }
+
+ public Bitmap get(String name, int size) {
+ return get(name,size,false);
+ }
+
+ public Bitmap get(final String name, final int size, boolean cachedOnly) {
+ final String KEY = key(name, size);
+ Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
+ if (bitmap != null || cachedOnly) {
+ return bitmap;
+ }
+ bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ final String trimmedName = name.trim();
+ final String letter = trimmedName.isEmpty() ? "X" : trimmedName.substring(0,1);
+ final int color = UIHelper.getColorForName(name);
+ drawTile(canvas, letter, color, 0, 0, size, size);
+ mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
+ return bitmap;
+ }
+
+ private String key(String name, int size) {
+ synchronized (this.sizes) {
+ if (!this.sizes.contains(size)) {
+ this.sizes.add(size);
+ }
+ }
+ return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size);
+ }
+
+ private void drawTile(Canvas canvas, String letter, int tileColor,
+ int left, int top, int right, int bottom) {
+ letter = letter.toUpperCase(Locale.getDefault());
+ Paint tilePaint = new Paint(), textPaint = new Paint();
+ tilePaint.setColor(tileColor);
+ textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
+ textPaint.setColor(FG_COLOR);
+ textPaint.setTypeface(Typeface.create("sans-serif-light",
+ Typeface.NORMAL));
+ textPaint.setTextSize((float) ((right - left) * 0.8));
+ Rect rect = new Rect();
+
+ canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
+ textPaint.getTextBounds(letter, 0, 1, rect);
+ float width = textPaint.measureText(letter);
+ canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom)
+ / 2 + rect.height() / 2, textPaint);
+ }
+
+ private void drawTile(Canvas canvas, MucOptions.User user, int left,
+ int top, int right, int bottom) {
+ Contact contact = user.getContact();
+ if (contact != null) {
+ Uri uri = null;
+ if (contact.getProfilePhoto() != null) {
+ uri = Uri.parse(contact.getProfilePhoto());
+ } else if (contact.getAvatar() != null) {
+ uri = mXmppConnectionService.getFileBackend().getAvatarUri(
+ contact.getAvatar());
+ }
+ if (uri != null) {
+ Bitmap bitmap = mXmppConnectionService.getFileBackend()
+ .cropCenter(uri, bottom - top, right - left);
+ if (bitmap != null) {
+ drawTile(canvas, bitmap, left, top, right, bottom);
+ return;
+ }
+ }
+ }
+ String name = contact != null ? contact.getDisplayName() : user.getName();
+ final String letter = name.isEmpty() ? "X" : name.substring(0,1);
+ final int color = UIHelper.getColorForName(name);
+ drawTile(canvas, letter, color, left, top, right, bottom);
+ }
+
+ private void drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop,
+ int dstright, int dstbottom) {
+ Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
+ canvas.drawBitmap(bm, null, dst, null);
+ }
+
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java b/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java
new file mode 100644
index 00000000..4367dd1b
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java
@@ -0,0 +1,24 @@
+package de.thedevstack.conversationsplus.services;
+
+import de.thedevstack.conversationsplus.persistance.DatabaseBackend;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class EventReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Intent mIntentForService = new Intent(context,
+ XmppConnectionService.class);
+ if (intent.getAction() != null) {
+ mIntentForService.setAction(intent.getAction());
+ } else {
+ mIntentForService.setAction("other");
+ }
+ if (intent.getAction().equals("ui")
+ || DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
+ context.startService(mIntentForService);
+ }
+ }
+
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java b/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java
new file mode 100644
index 00000000..8a6d1929
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java
@@ -0,0 +1,371 @@
+package de.thedevstack.conversationsplus.services;
+
+import android.util.Log;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.Config;
+import de.thedevstack.conversationsplus.R;
+import de.thedevstack.conversationsplus.entities.Account;
+import de.thedevstack.conversationsplus.entities.Conversation;
+import de.thedevstack.conversationsplus.generator.AbstractGenerator;
+import de.thedevstack.conversationsplus.xml.Element;
+import de.thedevstack.conversationsplus.xmpp.OnAdvancedStreamFeaturesLoaded;
+import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived;
+import de.thedevstack.conversationsplus.xmpp.jid.Jid;
+import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket;
+
+public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
+
+ private final XmppConnectionService mXmppConnectionService;
+
+ private final HashSet<Query> queries = new HashSet<Query>();
+ private final ArrayList<Query> pendingQueries = new ArrayList<Query>();
+
+ public enum PagingOrder {
+ NORMAL,
+ REVERSE
+ };
+
+ public MessageArchiveService(final XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public void catchup(final Account account) {
+ long startCatchup = getLastMessageTransmitted(account);
+ long endCatchup = account.getXmppConnection().getLastSessionEstablished();
+ if (startCatchup == 0) {
+ return;
+ } else if (endCatchup - startCatchup >= Config.MAM_MAX_CATCHUP) {
+ startCatchup = endCatchup - Config.MAM_MAX_CATCHUP;
+ List<Conversation> conversations = mXmppConnectionService.getConversations();
+ for (Conversation conversation : conversations) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted()) {
+ this.query(conversation,startCatchup);
+ }
+ }
+ }
+ final Query query = new Query(account, startCatchup, endCatchup);
+ this.queries.add(query);
+ this.execute(query);
+ }
+
+ private long getLastMessageTransmitted(final Account account) {
+ long timestamp = 0;
+ for(final Conversation conversation : mXmppConnectionService.getConversations()) {
+ if (conversation.getAccount() == account) {
+ long tmp = conversation.getLastMessageTransmitted();
+ if (tmp > timestamp) {
+ timestamp = tmp;
+ }
+ }
+ }
+ return timestamp;
+ }
+
+ public Query query(final Conversation conversation) {
+ return query(conversation,conversation.getAccount().getXmppConnection().getLastSessionEstablished());
+ }
+
+ public Query query(final Conversation conversation, long end) {
+ return this.query(conversation,conversation.getLastMessageTransmitted(),end);
+ }
+
+ public Query query(Conversation conversation, long start, long end) {
+ synchronized (this.queries) {
+ if (start > end) {
+ return null;
+ }
+ final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
+ this.queries.add(query);
+ this.execute(query);
+ return query;
+ }
+ }
+
+ public void executePendingQueries(final Account account) {
+ List<Query> pending = new ArrayList<>();
+ synchronized(this.pendingQueries) {
+ for(Iterator<Query> iterator = this.pendingQueries.iterator(); iterator.hasNext();) {
+ Query query = iterator.next();
+ if (query.getAccount() == account) {
+ pending.add(query);
+ iterator.remove();
+ }
+ }
+ }
+ for(Query query : pending) {
+ this.execute(query);
+ }
+ }
+
+ private void execute(final Query query) {
+ final Account account= query.getAccount();
+ if (account.getStatus() == Account.State.ONLINE) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": running mam query " + query.toString());
+ IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
+ this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.ERROR) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString());
+ finalizeQuery(query);
+ }
+ }
+ });
+ } else {
+ synchronized (this.pendingQueries) {
+ this.pendingQueries.add(query);
+ }
+ }
+ }
+
+ private void finalizeQuery(Query query) {
+ synchronized (this.queries) {
+ this.queries.remove(query);
+ }
+ final Conversation conversation = query.getConversation();
+ if (conversation != null) {
+ conversation.sort();
+ if (conversation.setLastMessageTransmitted(query.getEnd())) {
+ this.mXmppConnectionService.databaseBackend.updateConversation(conversation);
+ }
+ conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0);
+ if (query.hasCallback()) {
+ query.callback();
+ } else {
+ this.mXmppConnectionService.updateConversationUi();
+ }
+ } else {
+ for(Conversation tmp : this.mXmppConnectionService.getConversations()) {
+ if (tmp.getAccount() == query.getAccount()) {
+ tmp.sort();
+ if (tmp.setLastMessageTransmitted(query.getEnd())) {
+ this.mXmppConnectionService.databaseBackend.updateConversation(tmp);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) {
+ synchronized (this.queries) {
+ for(Query query : queries) {
+ if (query.conversation == conversation) {
+ if (!query.hasCallback() && callback != null) {
+ query.setCallback(callback);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ public void processFin(Element fin) {
+ if (fin == null) {
+ return;
+ }
+ Query query = findQuery(fin.getAttribute("queryid"));
+ if (query == null) {
+ return;
+ }
+ boolean complete = fin.getAttributeAsBoolean("complete");
+ Element set = fin.findChild("set","http://jabber.org/protocol/rsm");
+ Element last = set == null ? null : set.findChild("last");
+ Element first = set == null ? null : set.findChild("first");
+ Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first;
+ boolean abort = (query.getStart() == 0 && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES;
+ if (complete || relevant == null || abort) {
+ this.finalizeQuery(query);
+ Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages");
+ } else {
+ final Query nextQuery;
+ if (query.getPagingOrder() == PagingOrder.NORMAL) {
+ nextQuery = query.next(last == null ? null : last.getContent());
+ } else {
+ nextQuery = query.prev(first == null ? null : first.getContent());
+ }
+ this.execute(nextQuery);
+ this.finalizeQuery(query);
+ synchronized (this.queries) {
+ this.queries.remove(query);
+ this.queries.add(nextQuery);
+ }
+ }
+ }
+
+ public Query findQuery(String id) {
+ if (id == null) {
+ return null;
+ }
+ synchronized (this.queries) {
+ for(Query query : this.queries) {
+ if (query.getQueryId().equals(id)) {
+ return query;
+ }
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public void onAdvancedStreamFeaturesAvailable(Account account) {
+ if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) {
+ this.catchup(account);
+ }
+ }
+
+ public class Query {
+ private int totalCount = 0;
+ private int messageCount = 0;
+ private long start;
+ private long end;
+ private String queryId;
+ private String reference = null;
+ private Account account;
+ private Conversation conversation;
+ private PagingOrder pagingOrder = PagingOrder.NORMAL;
+ private XmppConnectionService.OnMoreMessagesLoaded callback = null;
+
+
+ public Query(Conversation conversation, long start, long end) {
+ this(conversation.getAccount(), start, end);
+ this.conversation = conversation;
+ }
+
+ public Query(Conversation conversation, long start, long end, PagingOrder order) {
+ this(conversation,start,end);
+ this.pagingOrder = order;
+ }
+
+ public Query(Account account, long start, long end) {
+ this.account = account;
+ this.start = start;
+ this.end = end;
+ this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
+ }
+
+ private Query page(String reference) {
+ Query query = new Query(this.account,this.start,this.end);
+ query.reference = reference;
+ query.conversation = conversation;
+ query.totalCount = totalCount;
+ query.callback = callback;
+ return query;
+ }
+
+ public Query next(String reference) {
+ Query query = page(reference);
+ query.pagingOrder = PagingOrder.NORMAL;
+ return query;
+ }
+
+ public Query prev(String reference) {
+ Query query = page(reference);
+ query.pagingOrder = PagingOrder.REVERSE;
+ return query;
+ }
+
+ public String getReference() {
+ return reference;
+ }
+
+ public PagingOrder getPagingOrder() {
+ return this.pagingOrder;
+ }
+
+ public String getQueryId() {
+ return queryId;
+ }
+
+ public Jid getWith() {
+ return conversation == null ? null : conversation.getJid().toBareJid();
+ }
+
+ public boolean muc() {
+ return conversation != null && conversation.getMode() == Conversation.MODE_MULTI;
+ }
+
+ public long getStart() {
+ return start;
+ }
+
+ public void setCallback(XmppConnectionService.OnMoreMessagesLoaded callback) {
+ this.callback = callback;
+ }
+
+ public void callback() {
+ if (this.callback != null) {
+ this.callback.onMoreMessagesLoaded(messageCount,conversation);
+ if (messageCount == 0) {
+ this.callback.informUser(R.string.no_more_history_on_server);
+ }
+ }
+ }
+
+ public long getEnd() {
+ return end;
+ }
+
+ public Conversation getConversation() {
+ return conversation;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public void incrementTotalCount() {
+ this.totalCount++;
+ }
+
+ public void incrementMessageCount() {
+ this.messageCount++;
+ }
+
+ public int getTotalCount() {
+ return this.totalCount;
+ }
+
+ public int getMessageCount() {
+ return this.messageCount;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ if (this.muc()) {
+ builder.append("to="+this.getWith().toString());
+ } else {
+ builder.append("with=");
+ if (this.getWith() == null) {
+ builder.append("*");
+ } else {
+ builder.append(getWith().toString());
+ }
+ }
+ builder.append(", start=");
+ builder.append(AbstractGenerator.getTimestamp(this.start));
+ builder.append(", end=");
+ builder.append(AbstractGenerator.getTimestamp(this.end));
+ if (this.reference!=null) {
+ if (this.pagingOrder == PagingOrder.NORMAL) {
+ builder.append(", after=");
+ } else {
+ builder.append(", before=");
+ }
+ builder.append(this.reference);
+ }
+ return builder.toString();
+ }
+
+ public boolean hasCallback() {
+ return this.callback != null;
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java
new file mode 100644
index 00000000..50cc793b
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java
@@ -0,0 +1,557 @@
+package de.thedevstack.conversationsplus.services;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.BigPictureStyle;
+import android.support.v4.app.NotificationCompat.Builder;
+import android.support.v4.app.TaskStackBuilder;
+import android.text.Html;
+import android.util.DisplayMetrics;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.tzur.conversations.Settings;
+import de.thedevstack.conversationsplus.Config;
+import de.thedevstack.conversationsplus.R;
+import de.thedevstack.conversationsplus.entities.Account;
+import de.thedevstack.conversationsplus.entities.Conversation;
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.ui.ConversationActivity;
+import de.thedevstack.conversationsplus.ui.ManageAccountActivity;
+import de.thedevstack.conversationsplus.ui.TimePreference;
+import de.thedevstack.conversationsplus.utils.GeoHelper;
+import de.thedevstack.conversationsplus.utils.UIHelper;
+
+public class NotificationService {
+
+ private final XmppConnectionService mXmppConnectionService;
+
+ private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
+
+ public static final int NOTIFICATION_ID = 0x2342;
+ public static final int FOREGROUND_NOTIFICATION_ID = 0x8899;
+ public static final int ERROR_NOTIFICATION_ID = 0x5678;
+
+ private Conversation mOpenConversation;
+ private boolean mIsInForeground;
+ private long mLastNotification;
+
+ public NotificationService(final XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ public boolean notify(final Message message) {
+ return (message.getStatus() == Message.STATUS_RECEIVED)
+ && notificationsEnabled()
+ && !message.getConversation().isMuted()
+ && (message.getConversation().getMode() == Conversation.MODE_SINGLE
+ || conferenceNotificationsEnabled()
+ || wasHighlightedOrPrivate(message)
+ );
+ }
+
+ public void notifyPebble(final Message message) {
+ final Intent i = new Intent("com.getpebble.action.SEND_NOTIFICATION");
+
+ final Conversation conversation = message.getConversation();
+ final JSONObject jsonData = new JSONObject(new HashMap<String, String>(2) {{
+ put("title", conversation.getName());
+ put("body", message.getBody());
+ }});
+ final String notificationData = new JSONArray().put(jsonData).toString();
+
+ i.putExtra("messageType", "PEBBLE_ALERT");
+ i.putExtra("sender", "Conversations"); /* XXX: Shouldn't be hardcoded, e.g., AbstractGenerator.APP_NAME); */
+ i.putExtra("notificationData", notificationData);
+
+ mXmppConnectionService.sendBroadcast(i);
+ }
+
+
+ public boolean notificationsEnabled() {
+ return mXmppConnectionService.getPreferences().getBoolean("show_notification", true);
+ }
+
+ public boolean isQuietHours() {
+ if (!mXmppConnectionService.getPreferences().getBoolean("enable_quiet_hours", false)) {
+ return false;
+ }
+ final long startTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
+ final long endTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY;
+ final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY;
+
+ if (endTime < startTime) {
+ return nowTime > startTime || nowTime < endTime;
+ } else {
+ return nowTime > startTime && nowTime < endTime;
+ }
+ }
+
+ public boolean conferenceNotificationsEnabled() {
+ return mXmppConnectionService.getPreferences().getBoolean("always_notify_in_conference", false);
+ }
+
+ @SuppressLint("NewApi")
+ @SuppressWarnings("deprecation")
+ private boolean isInteractive() {
+ final PowerManager pm = (PowerManager) mXmppConnectionService
+ .getSystemService(Context.POWER_SERVICE);
+
+ final boolean isScreenOn;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ isScreenOn = pm.isScreenOn();
+ } else {
+ isScreenOn = pm.isInteractive();
+ }
+
+ return isScreenOn;
+ }
+
+ public void push(final Message message) {
+ if (!notify(message)) {
+ return;
+ }
+
+ final boolean isScreenOn = isInteractive();
+
+ if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
+ return;
+ }
+
+ synchronized (notifications) {
+ final String conversationUuid = message.getConversationUuid();
+ if (notifications.containsKey(conversationUuid)) {
+ notifications.get(conversationUuid).add(message);
+ } else {
+ final ArrayList<Message> mList = new ArrayList<>();
+ mList.add(message);
+ notifications.put(conversationUuid, mList);
+ }
+ final Account account = message.getConversation().getAccount();
+ final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
+ && !account.inGracePeriod()
+ && !this.inMiniGracePeriod(account);
+ updateNotification(doNotify);
+ if (doNotify) {
+ notifyPebble(message);
+ }
+ }
+ }
+
+ public void clear() {
+ synchronized (notifications) {
+ notifications.clear();
+ updateNotification(false);
+ }
+ }
+
+ public void clear(final Conversation conversation) {
+ synchronized (notifications) {
+ notifications.remove(conversation.getUuid());
+ updateNotification(false);
+ }
+ }
+
+ private void setNotificationColor(final Builder mBuilder) {
+ mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary));
+ }
+
+ private void updateNotification(final boolean notify) {
+ final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ final SharedPreferences preferences = mXmppConnectionService.getPreferences();
+
+ final String ringtone = preferences.getString("notification_ringtone", null);
+ final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true);
+
+ if (notifications.size() == 0) {
+ notificationManager.cancel(NOTIFICATION_ID);
+ } else {
+ if (notify) {
+ this.markLastNotification();
+ }
+ final Builder mBuilder;
+ if (notifications.size() == 1) {
+ mBuilder = buildSingleConversations(notify);
+ } else {
+ mBuilder = buildMultipleConversation();
+ }
+ if (notify && !isQuietHours()) {
+ if (vibrate) {
+ final int dat = 70;
+ final long[] pattern = {0, 3 * dat, dat, dat};
+ mBuilder.setVibrate(pattern);
+ }
+ if (ringtone != null) {
+ mBuilder.setSound(Uri.parse(ringtone));
+ }
+ }
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
+ }
+ setNotificationColor(mBuilder);
+ mBuilder.setDefaults(0);
+ mBuilder.setSmallIcon(R.drawable.ic_notification);
+ mBuilder.setDeleteIntent(createDeleteIntent());
+ mBuilder.setLights(Settings.LED_COLOR, 2000, 4000);
+ final Notification notification = mBuilder.build();
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ }
+ }
+
+ private Builder buildMultipleConversation() {
+ final Builder mBuilder = new NotificationCompat.Builder(
+ mXmppConnectionService);
+ final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
+ style.setBigContentTitle(notifications.size()
+ + " "
+ + mXmppConnectionService
+ .getString(R.string.unread_conversations));
+ final StringBuilder names = new StringBuilder();
+ Conversation conversation = null;
+ for (final ArrayList<Message> messages : notifications.values()) {
+ if (messages.size() > 0) {
+ conversation = messages.get(0).getConversation();
+ final String name = conversation.getName();
+ style.addLine(Html.fromHtml("<b>" + name + "</b> "
+ + UIHelper.getMessagePreview(mXmppConnectionService,messages.get(0)).first));
+ names.append(name);
+ names.append(", ");
+ }
+ }
+ if (names.length() >= 2) {
+ names.delete(names.length() - 2, names.length());
+ }
+ mBuilder.setContentTitle(notifications.size()
+ + " "
+ + mXmppConnectionService
+ .getString(R.string.unread_conversations));
+ mBuilder.setContentText(names.toString());
+ mBuilder.setStyle(style);
+ if (conversation != null) {
+ mBuilder.setContentIntent(createContentIntent(conversation));
+ }
+ return mBuilder;
+ }
+
+ private Builder buildSingleConversations(final boolean notify) {
+ final Builder mBuilder = new NotificationCompat.Builder(
+ mXmppConnectionService);
+ final ArrayList<Message> messages = notifications.values().iterator().next();
+ if (messages.size() >= 1) {
+ final Conversation conversation = messages.get(0).getConversation();
+ mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
+ .get(conversation, getPixel(64)));
+ mBuilder.setContentTitle(conversation.getName());
+ Message message;
+ if ((message = getImage(messages)) != null) {
+ modifyForImage(mBuilder, message, messages, notify);
+ } else {
+ modifyForTextOnly(mBuilder, messages, notify);
+ }
+ if ((message = getFirstDownloadableMessage(messages)) != null) {
+ mBuilder.addAction(
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ?
+ R.drawable.ic_file_download_white_24dp : R.drawable.ic_action_download,
+ mXmppConnectionService.getResources().getString(R.string.download_x_file,
+ UIHelper.getFileDescriptionString(mXmppConnectionService, message)),
+ createDownloadIntent(message)
+ );
+ }
+ if ((message = getFirstLocationMessage(messages)) != null) {
+ mBuilder.addAction(R.drawable.ic_room_white_24dp,
+ mXmppConnectionService.getString(R.string.show_location),
+ createShowLocationIntent(message));
+ }
+ mBuilder.setContentIntent(createContentIntent(conversation));
+ }
+ return mBuilder;
+ }
+
+ private void modifyForImage(final Builder builder, final Message message,
+ final ArrayList<Message> messages, final boolean notify) {
+ try {
+ final Bitmap bitmap = mXmppConnectionService.getFileBackend()
+ .getThumbnail(message, getPixel(288), false);
+ final ArrayList<Message> tmp = new ArrayList<>();
+ for (final Message msg : messages) {
+ if (msg.getType() == Message.TYPE_TEXT
+ && msg.getDownloadable() == null) {
+ tmp.add(msg);
+ }
+ }
+ final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
+ bigPictureStyle.bigPicture(bitmap);
+ if (tmp.size() > 0) {
+ bigPictureStyle.setSummaryText(getMergedBodies(tmp));
+ builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService,tmp.get(0)).first);
+ } else {
+ builder.setContentText(mXmppConnectionService.getString(
+ R.string.received_x_file,
+ UIHelper.getFileDescriptionString(mXmppConnectionService,message)));
+ }
+ builder.setStyle(bigPictureStyle);
+ } catch (final FileNotFoundException e) {
+ modifyForTextOnly(builder, messages, notify);
+ }
+ }
+
+ private void modifyForTextOnly(final Builder builder,
+ final ArrayList<Message> messages, final boolean notify) {
+ builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
+ builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(0)).first);
+ if (notify) {
+ builder.setTicker(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(messages.size() - 1)).first);
+ }
+ }
+
+ private Message getImage(final Iterable<Message> messages) {
+ for (final Message message : messages) {
+ if (message.getType() == Message.TYPE_IMAGE
+ && message.getDownloadable() == null
+ && message.getEncryption() != Message.ENCRYPTION_PGP) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
+ for (final Message message : messages) {
+ if ((message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) &&
+ message.getDownloadable() != null) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private Message getFirstLocationMessage(final Iterable<Message> messages) {
+ for(final Message message : messages) {
+ if (GeoHelper.isGeoUri(message.getBody())) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private CharSequence getMergedBodies(final ArrayList<Message> messages) {
+ final StringBuilder text = new StringBuilder();
+ for (int i = 0; i < messages.size(); ++i) {
+ text.append(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(i)).first);
+ if (i != messages.size() - 1) {
+ text.append("\n");
+ }
+ }
+ return text.toString();
+ }
+
+ private PendingIntent createShowLocationIntent(final Message message) {
+ Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(message);
+ for(Intent intent : intents) {
+ if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) {
+ return PendingIntent.getActivity(mXmppConnectionService,18,intent,PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+ }
+ return createOpenConversationsIntent();
+ }
+
+ private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
+ final TaskStackBuilder stackBuilder = TaskStackBuilder
+ .create(mXmppConnectionService);
+ stackBuilder.addParentStack(ConversationActivity.class);
+
+ final Intent viewConversationIntent = new Intent(mXmppConnectionService,
+ ConversationActivity.class);
+ if (downloadMessageUuid != null) {
+ viewConversationIntent.setAction(ConversationActivity.ACTION_DOWNLOAD);
+ } else {
+ viewConversationIntent.setAction(Intent.ACTION_VIEW);
+ }
+ if (conversationUuid != null) {
+ viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid);
+ viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+ }
+ if (downloadMessageUuid != null) {
+ viewConversationIntent.putExtra(ConversationActivity.MESSAGE, downloadMessageUuid);
+ }
+
+ stackBuilder.addNextIntent(viewConversationIntent);
+
+ return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ private PendingIntent createDownloadIntent(final Message message) {
+ return createContentIntent(message.getConversationUuid(), message.getUuid());
+ }
+
+ private PendingIntent createContentIntent(final Conversation conversation) {
+ return createContentIntent(conversation.getUuid(), null);
+ }
+
+ private PendingIntent createDeleteIntent() {
+ final Intent intent = new Intent(mXmppConnectionService,
+ XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
+ return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
+ }
+
+ private PendingIntent createDisableForeground() {
+ final Intent intent = new Intent(mXmppConnectionService,
+ XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_DISABLE_FOREGROUND);
+ return PendingIntent.getService(mXmppConnectionService, 34, intent, 0);
+ }
+
+ private PendingIntent createTryAgainIntent() {
+ final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
+ return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
+ }
+
+ private PendingIntent createDisableAccountIntent(final Account account) {
+ final Intent intent = new Intent(mXmppConnectionService,XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_DISABLE_ACCOUNT);
+ intent.putExtra("account",account.getJid().toBareJid().toString());
+ return PendingIntent.getService(mXmppConnectionService,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ private boolean wasHighlightedOrPrivate(final Message message) {
+ final String nick = message.getConversation().getMucOptions().getActualNick();
+ final Pattern highlight = generateNickHighlightPattern(nick);
+ if (message.getBody() == null || nick == null) {
+ return false;
+ }
+ final Matcher m = highlight.matcher(message.getBody());
+ return (m.find() || message.getType() == Message.TYPE_PRIVATE);
+ }
+
+ private static Pattern generateNickHighlightPattern(final String nick) {
+ // We expect a word boundary, i.e. space or start of string, followed by
+ // the
+ // nick (matched in case-insensitive manner), followed by optional
+ // punctuation (for example "bob: i disagree" or "how are you alice?"),
+ // followed by another word boundary.
+ return Pattern.compile("\\b" + nick + "\\p{Punct}?\\b",
+ Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
+ }
+
+ public void setOpenConversation(final Conversation conversation) {
+ this.mOpenConversation = conversation;
+ }
+
+ public void setIsInForeground(final boolean foreground) {
+ this.mIsInForeground = foreground;
+ }
+
+ private int getPixel(final int dp) {
+ final DisplayMetrics metrics = mXmppConnectionService.getResources()
+ .getDisplayMetrics();
+ return ((int) (dp * metrics.density));
+ }
+
+ private void markLastNotification() {
+ this.mLastNotification = SystemClock.elapsedRealtime();
+ }
+
+ private boolean inMiniGracePeriod(final Account account) {
+ final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD
+ : Config.MINI_GRACE_PERIOD * 2;
+ return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
+ }
+
+ public Notification createForegroundNotification() {
+ final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
+
+ mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service));
+ mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations));
+ mBuilder.setContentIntent(createOpenConversationsIntent());
+ mBuilder.setWhen(0);
+ mBuilder.setPriority(NotificationCompat.PRIORITY_MIN);
+ final int cancelIcon;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mBuilder.setCategory(Notification.CATEGORY_SERVICE);
+ mBuilder.setSmallIcon(R.drawable.ic_import_export_white_48dp);
+ cancelIcon = R.drawable.ic_cancel_white_24dp;
+ } else {
+ mBuilder.setSmallIcon(R.drawable.ic_stat_communication_import_export);
+ cancelIcon = R.drawable.ic_action_cancel;
+ }
+ mBuilder.addAction(cancelIcon,
+ mXmppConnectionService.getString(R.string.disable_foreground_service),
+ createDisableForeground());
+ setNotificationColor(mBuilder);
+ return mBuilder.build();
+ }
+
+ private PendingIntent createOpenConversationsIntent() {
+ return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService,ConversationActivity.class),0);
+ }
+
+ public void updateErrorNotification() {
+ final NotificationManager mNotificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
+ final List<Account> errors = new ArrayList<>();
+ for (final Account account : mXmppConnectionService.getAccounts()) {
+ if (account.hasErrorStatus()) {
+ errors.add(account);
+ }
+ }
+ final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
+ if (errors.size() == 0) {
+ mNotificationManager.cancel(ERROR_NOTIFICATION_ID);
+ return;
+ } else if (errors.size() == 1) {
+ mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account));
+ mBuilder.setContentText(errors.get(0).getJid().toBareJid().toString());
+ } else {
+ mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts));
+ mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix));
+ }
+ mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
+ mXmppConnectionService.getString(R.string.try_again),
+ createTryAgainIntent());
+ if (errors.size() == 1) {
+ mBuilder.addAction(R.drawable.ic_block_white_24dp,
+ mXmppConnectionService.getString(R.string.disable_account),
+ createDisableAccountIntent(errors.get(0)));
+ }
+ mBuilder.setOngoing(true);
+ //mBuilder.setLights(0xffffffff, 2000, 4000);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mBuilder.setSmallIcon(R.drawable.ic_warning_white_36dp);
+ } else {
+ mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
+ }
+ final TaskStackBuilder stackBuilder = TaskStackBuilder.create(mXmppConnectionService);
+ stackBuilder.addParentStack(ConversationActivity.class);
+
+ final Intent manageAccountsIntent = new Intent(mXmppConnectionService,ManageAccountActivity.class);
+ stackBuilder.addNextIntent(manageAccountsIntent);
+
+ final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT);
+
+ mBuilder.setContentIntent(resultPendingIntent);
+ mNotificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build());
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java
new file mode 100644
index 00000000..3a78cac6
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java
@@ -0,0 +1,2434 @@
+package de.thedevstack.conversationsplus.services;
+
+import android.annotation.SuppressLint;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.FileObserver;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+import android.util.LruCache;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionStatus;
+
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpServiceConnection;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import de.duenndns.ssl.MemorizingTrustManager;
+import de.tzur.conversations.Settings;
+import de.thedevstack.conversationsplus.Config;
+import de.thedevstack.conversationsplus.R;
+import de.thedevstack.conversationsplus.crypto.PgpEngine;
+import de.thedevstack.conversationsplus.entities.Account;
+import de.thedevstack.conversationsplus.entities.Blockable;
+import de.thedevstack.conversationsplus.entities.Bookmark;
+import de.thedevstack.conversationsplus.entities.Contact;
+import de.thedevstack.conversationsplus.entities.Conversation;
+import de.thedevstack.conversationsplus.entities.Downloadable;
+import de.thedevstack.conversationsplus.entities.DownloadablePlaceholder;
+import de.thedevstack.conversationsplus.entities.Message;
+import de.thedevstack.conversationsplus.entities.MucOptions;
+import de.thedevstack.conversationsplus.entities.MucOptions.OnRenameListener;
+import de.thedevstack.conversationsplus.entities.Presences;
+import de.thedevstack.conversationsplus.generator.IqGenerator;
+import de.thedevstack.conversationsplus.generator.MessageGenerator;
+import de.thedevstack.conversationsplus.generator.PresenceGenerator;
+import de.thedevstack.conversationsplus.http.HttpConnectionManager;
+import de.thedevstack.conversationsplus.parser.IqParser;
+import de.thedevstack.conversationsplus.parser.MessageParser;
+import de.thedevstack.conversationsplus.parser.PresenceParser;
+import de.thedevstack.conversationsplus.persistance.DatabaseBackend;
+import de.thedevstack.conversationsplus.persistance.FileBackend;
+import de.thedevstack.conversationsplus.ui.UiCallback;
+import de.thedevstack.conversationsplus.utils.CryptoHelper;
+import de.thedevstack.conversationsplus.utils.ExceptionHelper;
+import de.thedevstack.conversationsplus.utils.OnPhoneContactsLoadedListener;
+import de.thedevstack.conversationsplus.utils.PRNGFixes;
+import de.thedevstack.conversationsplus.utils.PhoneHelper;
+import de.thedevstack.conversationsplus.utils.Xmlns;
+import de.thedevstack.conversationsplus.xml.Element;
+import de.thedevstack.conversationsplus.xmpp.OnBindListener;
+import de.thedevstack.conversationsplus.xmpp.OnContactStatusChanged;
+import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived;
+import de.thedevstack.conversationsplus.xmpp.OnMessageAcknowledged;
+import de.thedevstack.conversationsplus.xmpp.OnMessagePacketReceived;
+import de.thedevstack.conversationsplus.xmpp.OnPresencePacketReceived;
+import de.thedevstack.conversationsplus.xmpp.OnStatusChanged;
+import de.thedevstack.conversationsplus.xmpp.OnUpdateBlocklist;
+import de.thedevstack.conversationsplus.xmpp.XmppConnection;
+import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState;
+import de.thedevstack.conversationsplus.xmpp.forms.Data;
+import de.thedevstack.conversationsplus.xmpp.forms.Field;
+import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException;
+import de.thedevstack.conversationsplus.xmpp.jid.Jid;
+import de.thedevstack.conversationsplus.xmpp.jingle.JingleConnectionManager;
+import de.thedevstack.conversationsplus.xmpp.jingle.OnJinglePacketReceived;
+import de.thedevstack.conversationsplus.xmpp.jingle.stanzas.JinglePacket;
+import de.thedevstack.conversationsplus.xmpp.pep.Avatar;
+import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket;
+import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket;
+import de.thedevstack.conversationsplus.xmpp.stanzas.PresencePacket;
+
+public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener {
+
+ public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
+ public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground";
+ private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
+ public static final String ACTION_TRY_AGAIN = "try_again";
+ public static final String ACTION_DISABLE_ACCOUNT = "disable_account";
+ private ContentObserver contactObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ Intent intent = new Intent(getApplicationContext(),
+ XmppConnectionService.class);
+ intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
+ startService(intent);
+ }
+ };
+ private final IBinder mBinder = new XmppConnectionBinder();
+ private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
+ private final FileObserver fileObserver = new FileObserver(
+ FileBackend.getConversationsImageDirectory()) {
+
+ @Override
+ public void onEvent(int event, String path) {
+ if (event == FileObserver.DELETE) {
+ markFileDeleted(path.split("\\.")[0]);
+ }
+ }
+ };
+ private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
+
+ @Override
+ public void onJinglePacketReceived(Account account, JinglePacket packet) {
+ mJingleConnectionManager.deliverPacket(account, packet);
+ }
+ };
+ private final OnBindListener mOnBindListener = new OnBindListener() {
+
+ @Override
+ public void onBind(final Account account) {
+ account.getRoster().clearPresences();
+ account.pendingConferenceJoins.clear();
+ account.pendingConferenceLeaves.clear();
+ fetchRosterFromServer(account);
+ fetchBookmarks(account);
+ sendPresence(account);
+ connectMultiModeConversations(account);
+ updateConversationUi();
+ }
+ };
+ private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
+
+ @Override
+ public void onMessageAcknowledged(Account account, String uuid) {
+ for (final Conversation conversation : getConversations()) {
+ if (conversation.getAccount() == account) {
+ Message message = conversation.findUnsentMessageWithUuid(uuid);
+ if (message != null) {
+ markMessage(message, Message.STATUS_SEND);
+ if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) {
+ databaseBackend.updateConversation(conversation);
+ }
+ }
+ }
+ }
+ }
+ };
+ private final IqGenerator mIqGenerator = new IqGenerator(this);
+ public DatabaseBackend databaseBackend;
+ public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
+
+ @Override
+ public void onContactStatusChanged(Contact contact, boolean online) {
+ Conversation conversation = find(getConversations(), contact);
+ if (conversation != null) {
+ if (online && contact.getPresences().size() > 1) {
+ conversation.endOtrIfNeeded();
+ } else {
+ conversation.resetOtrSession();
+ }
+ if (online && (contact.getPresences().size() == 1)) {
+ sendUnsentMessages(conversation);
+ }
+ }
+ }
+ };
+ private FileBackend fileBackend = new FileBackend(this);
+ private MemorizingTrustManager mMemorizingTrustManager;
+ private NotificationService mNotificationService = new NotificationService(
+ this);
+ private OnMessagePacketReceived mMessageParser = new MessageParser(this);
+ private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
+ private IqParser mIqParser = new IqParser(this);
+ private MessageGenerator mMessageGenerator = new MessageGenerator(this);
+ private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
+ private List<Account> accounts;
+ private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
+ this);
+ private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
+ this);
+ private AvatarService mAvatarService = new AvatarService(this);
+ private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
+ private OnConversationUpdate mOnConversationUpdate = null;
+ private Integer convChangedListenerCount = 0;
+ private OnAccountUpdate mOnAccountUpdate = null;
+ private OnStatusChanged statusListener = new OnStatusChanged() {
+
+ @Override
+ public void onStatusChanged(Account account) {
+ XmppConnection connection = account.getXmppConnection();
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ }
+ if (account.getStatus() == Account.State.ONLINE) {
+ for (Conversation conversation : account.pendingConferenceLeaves) {
+ leaveMuc(conversation);
+ }
+ for (Conversation conversation : account.pendingConferenceJoins) {
+ joinMuc(conversation);
+ }
+ mMessageArchiveService.executePendingQueries(account);
+ mJingleConnectionManager.cancelInTransmission();
+ List<Conversation> conversations = getConversations();
+ for (Conversation conversation : conversations) {
+ if (conversation.getAccount() == account) {
+ conversation.startOtrIfNeeded();
+ sendUnsentMessages(conversation);
+ }
+ }
+ if (connection != null && connection.getFeatures().csi()) {
+ if (checkListeners()) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + " sending csi//inactive");
+ connection.sendInactive();
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + " sending csi//active");
+ connection.sendActive();
+ }
+ }
+ syncDirtyContacts(account);
+ scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode());
+ } else if (account.getStatus() == Account.State.OFFLINE) {
+ resetSendingToWaiting(account);
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ int timeToReconnect = mRandom.nextInt(50) + 10;
+ scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode());
+ }
+ } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
+ databaseBackend.updateAccount(account);
+ reconnectAccount(account, true);
+ } else if ((account.getStatus() != Account.State.CONNECTING)
+ && (account.getStatus() != Account.State.NO_INTERNET)) {
+ if (connection != null) {
+ int next = connection.getTimeToNextAttempt();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": error connecting account. try again in "
+ + next + "s for the "
+ + (connection.getAttempt() + 1) + " time");
+ scheduleWakeUpCall(next,account.getUuid().hashCode());
+ }
+ }
+ getNotificationService().updateErrorNotification();
+ }
+ };
+ private int accountChangedListenerCount = 0;
+ private OnRosterUpdate mOnRosterUpdate = null;
+ private OnUpdateBlocklist mOnUpdateBlocklist = null;
+ private int updateBlocklistListenerCount = 0;
+ private int rosterChangedListenerCount = 0;
+ private OnMucRosterUpdate mOnMucRosterUpdate = null;
+ private int mucRosterChangedListenerCount = 0;
+ private SecureRandom mRandom;
+ private OpenPgpServiceConnection pgpServiceConnection;
+ private PgpEngine mPgpEngine = null;
+ private WakeLock wakeLock;
+ private PowerManager pm;
+ private LruCache<String, Bitmap> mBitmapCache;
+ private Thread mPhoneContactMergerThread;
+
+ private boolean mRestoredFromDatabase = false;
+ public boolean areMessagesInitialized() {
+ return this.mRestoredFromDatabase;
+ }
+
+ public PgpEngine getPgpEngine() {
+ if (pgpServiceConnection.isBound()) {
+ if (this.mPgpEngine == null) {
+ this.mPgpEngine = new PgpEngine(new OpenPgpApi(
+ getApplicationContext(),
+ pgpServiceConnection.getService()), this);
+ }
+ return mPgpEngine;
+ } else {
+ return null;
+ }
+
+ }
+
+ public FileBackend getFileBackend() {
+ return this.fileBackend;
+ }
+
+ public AvatarService getAvatarService() {
+ return this.mAvatarService;
+ }
+
+ public void attachLocationToConversation(final Conversation conversation,
+ final Uri uri,
+ final UiCallback<Message> callback) {
+ int encryption = conversation.getNextEncryption(forceEncryption());
+ if (encryption == Message.ENCRYPTION_PGP) {
+ encryption = Message.ENCRYPTION_DECRYPTED;
+ }
+ Message message = new Message(conversation,uri.toString(),encryption);
+ if (conversation.getNextCounterpart() != null) {
+ message.setCounterpart(conversation.getNextCounterpart());
+ }
+ if (encryption == Message.ENCRYPTION_DECRYPTED) {
+ getPgpEngine().encrypt(message,callback);
+ } else {
+ callback.success(message);
+ }
+ }
+
+ public void attachFileToConversation(final Conversation conversation,
+ final Uri uri,
+ final UiCallback<Message> callback) {
+ final Message message;
+ if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ message = new Message(conversation, "",
+ Message.ENCRYPTION_DECRYPTED);
+ } else {
+ message = new Message(conversation, "",
+ conversation.getNextEncryption(forceEncryption()));
+ }
+ message.setCounterpart(conversation.getNextCounterpart());
+ message.setType(Message.TYPE_FILE);
+ message.setStatus(Message.STATUS_OFFERED);
+ String path = getFileBackend().getOriginalPath(uri);
+ if (path!=null) {
+ message.setRelativeFilePath(path);
+ getFileBackend().updateFileParams(message);
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ getPgpEngine().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } else {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ getFileBackend().copyFileToPrivateStorage(message, uri);
+ getFileBackend().updateFileParams(message);
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ getPgpEngine().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } catch (FileBackend.FileCopyException e) {
+ callback.error(e.getResId(),message);
+ }
+ }
+ }).start();
+
+ }
+ }
+
+ public void attachImageToConversation(final Conversation conversation,
+ final Uri uri, final UiCallback<Message> callback) {
+ final Message message;
+ if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ message = new Message(conversation, "",
+ Message.ENCRYPTION_DECRYPTED);
+ } else {
+ message = new Message(conversation, "",
+ conversation.getNextEncryption(forceEncryption()));
+ }
+ message.setCounterpart(conversation.getNextCounterpart());
+ message.setType(Message.TYPE_IMAGE);
+ message.setStatus(Message.STATUS_OFFERED);
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ getFileBackend().copyImageToPrivateStorage(message, uri);
+ if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ getPgpEngine().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } catch (final FileBackend.FileCopyException e) {
+ callback.error(e.getResId(), message);
+ }
+ }
+ }).start();
+ }
+
+ public Conversation find(Bookmark bookmark) {
+ return find(bookmark.getAccount(), bookmark.getJid());
+ }
+
+ public Conversation find(final Account account, final Jid jid) {
+ return find(getConversations(), account, jid);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ final String action = intent == null ? null : intent.getAction();
+ if (action != null) {
+ switch (action) {
+ case ACTION_MERGE_PHONE_CONTACTS:
+ if (mRestoredFromDatabase) {
+ PhoneHelper.loadPhoneContacts(getApplicationContext(),
+ new CopyOnWriteArrayList<Bundle>(),
+ this);
+ }
+ return START_STICKY;
+ case Intent.ACTION_SHUTDOWN:
+ logoutAndSave();
+ return START_NOT_STICKY;
+ case ACTION_CLEAR_NOTIFICATION:
+ mNotificationService.clear();
+ break;
+ case ACTION_DISABLE_FOREGROUND:
+ getPreferences().edit().putBoolean("keep_foreground_service",false).commit();
+ toggleForegroundService();
+ break;
+ case ACTION_TRY_AGAIN:
+ for(Account account : accounts) {
+ if (account.hasErrorStatus()) {
+ final XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.resetAttemptCount();
+ }
+ }
+ }
+ break;
+ case ACTION_DISABLE_ACCOUNT:
+ try {
+ String jid = intent.getStringExtra("account");
+ Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid));
+ if (account != null) {
+ account.setOption(Account.OPTION_DISABLED,true);
+ updateAccount(account);
+ }
+ } catch (final InvalidJidException ignored) {
+ break;
+ }
+ break;
+ }
+ }
+ this.wakeLock.acquire();
+
+ for (Account account : accounts) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (!hasInternetConnection()) {
+ account.setStatus(Account.State.NO_INTERNET);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ } else {
+ if (account.getStatus() == Account.State.NO_INTERNET) {
+ account.setStatus(Account.State.OFFLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ if (account.getStatus() == Account.State.ONLINE) {
+ long lastReceived = account.getXmppConnection().getLastPacketReceived();
+ long lastSent = account.getXmppConnection().getLastPingSent();
+ long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
+ long msToNextPing = (Math.max(lastReceived,lastSent) + pingInterval) - SystemClock.elapsedRealtime();
+ if (lastSent > lastReceived && (lastSent + Config.PING_TIMEOUT * 1000) < SystemClock.elapsedRealtime()) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": ping timeout");
+ this.reconnectAccount(account, true);
+ } else if (msToNextPing <= 0) {
+ account.getXmppConnection().sendPing();
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()+" send ping");
+ this.scheduleWakeUpCall(Config.PING_TIMEOUT,account.getUuid().hashCode());
+ } else {
+ this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
+ }
+ } else if (account.getStatus() == Account.State.OFFLINE) {
+ reconnectAccount(account,true);
+ } else if (account.getStatus() == Account.State.CONNECTING) {
+ long timeout = Config.CONNECT_TIMEOUT - ((SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000);
+ if (timeout < 0) {
+ Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
+ reconnectAccount(account, true);
+ } else {
+ scheduleWakeUpCall((int) timeout,account.getUuid().hashCode());
+ }
+ } else {
+ if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
+ reconnectAccount(account, true);
+ }
+ }
+
+ }
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ }
+ }
+ }
+ /*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
+ if (!pm.isScreenOn()) {
+ removeStaleListeners();
+ }*/
+ if (wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (final RuntimeException ignored) {
+ }
+ }
+ return START_STICKY;
+ }
+
+ public boolean hasInternetConnection() {
+ ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+ return activeNetwork != null && activeNetwork.isConnected();
+ }
+
+ /**
+ * check whether we are allowed to download at the moment
+ */
+ public boolean isDownloadAllowedInConnection() {
+ if (Settings.DOWNLOAD_ONLY_WLAN) {
+ return isWifiConnected();
+ }
+ return true;
+ }
+
+ /**
+ * check whether wifi is connected
+ */
+ public boolean isWifiConnected() {
+ ConnectivityManager cm = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo niWifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+ return niWifi.isConnected();
+ }
+
+ @SuppressLint("TrulyRandom")
+ @Override
+ public void onCreate() {
+ ExceptionHelper.init(getApplicationContext());
+ PRNGFixes.apply();
+ this.mRandom = new SecureRandom();
+ this.mMemorizingTrustManager = new MemorizingTrustManager(
+ getApplicationContext());
+
+ final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ final int cacheSize = maxMemory / 8;
+ this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
+ @Override
+ protected int sizeOf(final String key, final Bitmap bitmap) {
+ return bitmap.getByteCount() / 1024;
+ }
+ };
+
+ this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
+ this.accounts = databaseBackend.getAccounts();
+
+ for (final Account account : this.accounts) {
+ account.initOtrEngine(this);
+ }
+ restoreFromDatabase();
+
+ getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
+ this.fileObserver.startWatching();
+ this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain");
+ this.pgpServiceConnection.bindToService();
+
+ this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"XmppConnectionService");
+ toggleForegroundService();
+ }
+
+ public void toggleForegroundService() {
+ if (getPreferences().getBoolean("keep_foreground_service",false)) {
+ startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
+ } else {
+ stopForeground(true);
+ }
+ }
+
+ @Override
+ public void onTaskRemoved(final Intent rootIntent) {
+ super.onTaskRemoved(rootIntent);
+ if (!getPreferences().getBoolean("keep_foreground_service",false)) {
+ this.logoutAndSave();
+ }
+ }
+
+ private void logoutAndSave() {
+ for (final Account account : accounts) {
+ databaseBackend.writeRoster(account.getRoster());
+ if (account.getXmppConnection() != null) {
+ disconnect(account, false);
+ }
+ }
+ Context context = getApplicationContext();
+ AlarmManager alarmManager = (AlarmManager) context
+ .getSystemService(Context.ALARM_SERVICE);
+ Intent intent = new Intent(context, EventReceiver.class);
+ alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
+ Log.d(Config.LOGTAG, "good bye");
+ stopSelf();
+ }
+
+ protected void scheduleWakeUpCall(int seconds, int requestCode) {
+ final long timeToWake = SystemClock.elapsedRealtime() + (seconds + 1) * 1000;
+
+ Context context = getApplicationContext();
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+
+ Intent intent = new Intent(context, EventReceiver.class);
+ intent.setAction("ping");
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(context, requestCode, intent, 0);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent);
+ }
+
+ public XmppConnection createConnection(final Account account) {
+ final SharedPreferences sharedPref = getPreferences();
+ account.setResource(sharedPref.getString("resource", "mobile")
+ .toLowerCase(Locale.getDefault()));
+ final XmppConnection connection = new XmppConnection(account, this);
+ connection.setOnMessagePacketReceivedListener(this.mMessageParser);
+ connection.setOnStatusChangedListener(this.statusListener);
+ connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
+ connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
+ connection.setOnJinglePacketReceivedListener(this.jingleListener);
+ connection.setOnBindListener(this.mOnBindListener);
+ connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
+ connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
+ return connection;
+ }
+
+ public void sendChatState(Conversation conversation) {
+ if (sendChatStates()) {
+ MessagePacket packet = mMessageGenerator.generateChatState(conversation);
+ sendMessagePacket(conversation.getAccount(), packet);
+ }
+ }
+
+ public void sendMessage(final Message message) {
+ final Account account = message.getConversation().getAccount();
+ account.deactivateGracePeriod();
+ final Conversation conv = message.getConversation();
+ MessagePacket packet = null;
+ boolean saveInDb = true;
+ boolean send = false;
+ if (account.getStatus() == Account.State.ONLINE
+ && account.getXmppConnection() != null) {
+ if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
+ if (message.getCounterpart() != null) {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (!conv.hasValidOtrSession()) {
+ conv.startOtrSession(message.getCounterpart().getResourcepart(),true);
+ message.setStatus(Message.STATUS_WAITING);
+ } else if (conv.hasValidOtrSession()
+ && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+ mJingleConnectionManager
+ .createNewConnection(message);
+ }
+ } else {
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ conv.startOtrIfNeeded();
+ }
+ message.setStatus(Message.STATUS_WAITING);
+ }
+ } else {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (!conv.hasValidOtrSession() && (message.getCounterpart() != null)) {
+ conv.startOtrSession(message.getCounterpart().getResourcepart(), true);
+ message.setStatus(Message.STATUS_WAITING);
+ } else if (conv.hasValidOtrSession()) {
+ if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+ packet = mMessageGenerator.generateOtrChat(message);
+ send = true;
+ } else {
+ message.setStatus(Message.STATUS_WAITING);
+ conv.startOtrIfNeeded();
+ }
+ } else {
+ message.setStatus(Message.STATUS_WAITING);
+ }
+ } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ message.getConversation().endOtrIfNeeded();
+ message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
+ @Override
+ public void onMessageFound(Message message) {
+ markMessage(message,Message.STATUS_SEND_FAILED);
+ }
+ });
+ packet = mMessageGenerator.generatePgpChat(message);
+ send = true;
+ } else {
+ message.getConversation().endOtrIfNeeded();
+ message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
+ @Override
+ public void onMessageFound(Message message) {
+ markMessage(message,Message.STATUS_SEND_FAILED);
+ }
+ });
+ packet = mMessageGenerator.generateChat(message);
+ send = true;
+ }
+ }
+ if (!account.getXmppConnection().getFeatures().sm()
+ && conv.getMode() != Conversation.MODE_MULTI) {
+ message.setStatus(Message.STATUS_SEND);
+ }
+ } else {
+ message.setStatus(Message.STATUS_WAITING);
+ if (message.getType() == Message.TYPE_TEXT) {
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ String pgpBody = message.getEncryptedBody();
+ String decryptedBody = message.getBody();
+ message.setBody(pgpBody);
+ message.setEncryption(Message.ENCRYPTION_PGP);
+ databaseBackend.createMessage(message);
+ saveInDb = false;
+ message.setBody(decryptedBody);
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (!conv.hasValidOtrSession()
+ && message.getCounterpart() != null) {
+ conv.startOtrSession(message.getCounterpart().getResourcepart(), false);
+ }
+ }
+ }
+
+ }
+ conv.add(message);
+ if (saveInDb) {
+ if (message.getEncryption() == Message.ENCRYPTION_NONE
+ || saveEncryptedMessages()) {
+ databaseBackend.createMessage(message);
+ }
+ }
+ if ((send) && (packet != null)) {
+ if (conv.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
+ if (this.sendChatStates()) {
+ packet.addChild(ChatState.toElement(conv.getOutgoingChatState()));
+ }
+ }
+ sendMessagePacket(account, packet);
+ }
+ updateConversationUi();
+ }
+
+ private void sendUnsentMessages(final Conversation conversation) {
+ conversation.findWaitingMessages(new Conversation.OnMessageFound() {
+
+ @Override
+ public void onMessageFound(Message message) {
+ resendMessage(message);
+ }
+ });
+ }
+
+ private void resendMessage(final Message message) {
+ Account account = message.getConversation().getAccount();
+ MessagePacket packet = null;
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ Presences presences = message.getConversation().getContact()
+ .getPresences();
+ if (!message.getConversation().hasValidOtrSession()) {
+ if ((message.getCounterpart() != null)
+ && (presences.has(message.getCounterpart().getResourcepart()))) {
+ message.getConversation().startOtrSession(message.getCounterpart().getResourcepart(), true);
+ } else {
+ if (presences.size() == 1) {
+ String presence = presences.asStringArray()[0];
+ message.getConversation().startOtrSession(presence, true);
+ }
+ }
+ } else {
+ if (message.getConversation().getOtrSession()
+ .getSessionStatus() == SessionStatus.ENCRYPTED) {
+ try {
+ message.setCounterpart(Jid.fromSessionID(message.getConversation().getOtrSession().getSessionID()));
+ if (message.getType() == Message.TYPE_TEXT) {
+ packet = mMessageGenerator.generateOtrChat(message,
+ true);
+ } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ } catch (final InvalidJidException ignored) {
+
+ }
+ }
+ }
+ } else if (message.getType() == Message.TYPE_TEXT) {
+ if (message.getEncryption() == Message.ENCRYPTION_NONE) {
+ packet = mMessageGenerator.generateChat(message, true);
+ } else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
+ || (message.getEncryption() == Message.ENCRYPTION_PGP)) {
+ packet = mMessageGenerator.generatePgpChat(message, true);
+ }
+ } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
+ Contact contact = message.getConversation().getContact();
+ Presences presences = contact.getPresences();
+ if ((message.getCounterpart() != null)
+ && (presences.has(message.getCounterpart().getResourcepart()))) {
+ markMessage(message, Message.STATUS_OFFERED);
+ mJingleConnectionManager.createNewConnection(message);
+ } else {
+ if (presences.size() == 1) {
+ String presence = presences.asStringArray()[0];
+ try {
+ message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence));
+ } catch (InvalidJidException e) {
+ return;
+ }
+ markMessage(message, Message.STATUS_OFFERED);
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ }
+ }
+ if (packet != null) {
+ if (!account.getXmppConnection().getFeatures().sm()
+ && message.getConversation().getMode() != Conversation.MODE_MULTI) {
+ markMessage(message, Message.STATUS_SEND);
+ } else {
+ markMessage(message, Message.STATUS_UNSEND);
+ }
+ if (message.getConversation().setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
+ if (this.sendChatStates()) {
+ packet.addChild(ChatState.toElement(message.getConversation().getOutgoingChatState()));
+ }
+ }
+ sendMessagePacket(account, packet);
+ }
+ }
+
+ public void fetchRosterFromServer(final Account account) {
+ final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
+ if (!"".equals(account.getRosterVersion())) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": fetching roster version " + account.getRosterVersion());
+ } else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
+ }
+ iqPacket.query(Xmlns.ROSTER).setAttribute("ver",
+ account.getRosterVersion());
+ account.getXmppConnection().sendIqPacket(iqPacket, mIqParser);
+ }
+
+ public void fetchBookmarks(final Account account) {
+ final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
+ final Element query = iqPacket.query("jabber:iq:private");
+ query.addChild("storage", "storage:bookmarks");
+ final OnIqPacketReceived callback = new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ final Element query = packet.query();
+ final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
+ final Element storage = query.findChild("storage",
+ "storage:bookmarks");
+ if (storage != null) {
+ for (final Element item : storage.getChildren()) {
+ if (item.getName().equals("conference")) {
+ final Bookmark bookmark = Bookmark.parse(item, account);
+ bookmarks.add(bookmark);
+ Conversation conversation = find(bookmark);
+ if (conversation != null) {
+ conversation.setBookmark(bookmark);
+ } else if (bookmark.autojoin() && bookmark.getJid() != null) {
+ conversation = findOrCreateConversation(
+ account, bookmark.getJid(), true);
+ conversation.setBookmark(bookmark);
+ joinMuc(conversation);
+ }
+ }
+ }
+ }
+ account.setBookmarks(bookmarks);
+ }
+ };
+ sendIqPacket(account, iqPacket, callback);
+ }
+
+ public void pushBookmarks(Account account) {
+ IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
+ Element query = iqPacket.query("jabber:iq:private");
+ Element storage = query.addChild("storage", "storage:bookmarks");
+ for (Bookmark bookmark : account.getBookmarks()) {
+ storage.addChild(bookmark);
+ }
+ sendIqPacket(account, iqPacket, null);
+ }
+
+ public void onPhoneContactsLoaded(final List<Bundle> phoneContacts) {
+ if (mPhoneContactMergerThread != null) {
+ mPhoneContactMergerThread.interrupt();
+ }
+ mPhoneContactMergerThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Log.d(Config.LOGTAG,"start merging phone contacts with roster");
+ for (Account account : accounts) {
+ account.getRoster().clearSystemAccounts();
+ for (Bundle phoneContact : phoneContacts) {
+ if (Thread.interrupted()) {
+ Log.d(Config.LOGTAG,"interrupted merging phone contacts");
+ return;
+ }
+ Jid jid;
+ try {
+ jid = Jid.fromString(phoneContact.getString("jid"));
+ } catch (final InvalidJidException e) {
+ continue;
+ }
+ final Contact contact = account.getRoster().getContact(jid);
+ String systemAccount = phoneContact.getInt("phoneid")
+ + "#"
+ + phoneContact.getString("lookup");
+ contact.setSystemAccount(systemAccount);
+ contact.setPhotoUri(phoneContact.getString("photouri"));
+ getAvatarService().clear(contact);
+ contact.setSystemName(phoneContact.getString("displayname"));
+ }
+ }
+ Log.d(Config.LOGTAG,"finished merging phone contacts");
+ updateAccountUi();
+ }
+ });
+ mPhoneContactMergerThread.start();
+ }
+
+ private void restoreFromDatabase() {
+ synchronized (this.conversations) {
+ final Map<String, Account> accountLookupTable = new Hashtable<>();
+ for (Account account : this.accounts) {
+ accountLookupTable.put(account.getUuid(), account);
+ }
+ this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
+ for (Conversation conversation : this.conversations) {
+ Account account = accountLookupTable.get(conversation.getAccountUuid());
+ conversation.setAccount(account);
+ }
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Log.d(Config.LOGTAG,"restoring roster");
+ for(Account account : accounts) {
+ databaseBackend.readRoster(account.getRoster());
+ }
+ getBitmapCache().evictAll();
+ Looper.prepare();
+ PhoneHelper.loadPhoneContacts(getApplicationContext(),
+ new CopyOnWriteArrayList<Bundle>(),
+ XmppConnectionService.this);
+ Log.d(Config.LOGTAG,"restoring messages");
+ for (Conversation conversation : conversations) {
+ conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
+ checkDeletedFiles(conversation);
+ }
+ mRestoredFromDatabase = true;
+ Log.d(Config.LOGTAG,"restored all messages");
+ updateConversationUi();
+ }
+ }).start();
+ }
+ }
+
+ public List<Conversation> getConversations() {
+ return this.conversations;
+ }
+
+ private void checkDeletedFiles(Conversation conversation) {
+ conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
+
+ @Override
+ public void onMessageFound(Message message) {
+ if (!getFileBackend().isFileAvailable(message)) {
+ message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
+ }
+ }
+ });
+ }
+
+ private void markFileDeleted(String uuid) {
+ for (Conversation conversation : getConversations()) {
+ Message message = conversation.findMessageWithFileAndUuid(uuid);
+ if (message != null) {
+ if (!getFileBackend().isFileAvailable(message)) {
+ message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
+ updateConversationUi();
+ }
+ return;
+ }
+ }
+ }
+
+ public void populateWithOrderedConversations(final List<Conversation> list) {
+ populateWithOrderedConversations(list, true);
+ }
+
+ public void populateWithOrderedConversations(final List<Conversation> list, boolean includeConferences) {
+ list.clear();
+ if (includeConferences) {
+ list.addAll(getConversations());
+ } else {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ list.add(conversation);
+ }
+ }
+ }
+ Collections.sort(list, new Comparator<Conversation>() {
+ @Override
+ public int compare(Conversation lhs, Conversation rhs) {
+ Message left = lhs.getLatestMessage();
+ Message right = rhs.getLatestMessage();
+ if (left.getTimeSent() > right.getTimeSent()) {
+ return -1;
+ } else if (left.getTimeSent() < right.getTimeSent()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ });
+ }
+
+ public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
+ Log.d(Config.LOGTAG,"load more messages for "+conversation.getName() + " prior to "+MessageGenerator.getTimestamp(timestamp));
+ if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) {
+ return;
+ }
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ final Account account = conversation.getAccount();
+ List<Message> messages = databaseBackend.getMessages(conversation, 50,timestamp);
+ if (messages.size() > 0) {
+ conversation.addAll(0, messages);
+ checkDeletedFiles(conversation);
+ callback.onMoreMessagesLoaded(messages.size(), conversation);
+ } else if (conversation.hasMessagesLeftOnServer()
+ && account.isOnlineAndConnected()
+ && account.getXmppConnection().getFeatures().mam()) {
+ MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1);
+ if (query != null) {
+ query.setCallback(callback);
+ }
+ callback.informUser(R.string.fetching_history_from_server);
+ }
+ }
+ }).start();
+ }
+
+ public List<Account> getAccounts() {
+ return this.accounts;
+ }
+
+ public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
+ for (final Conversation conversation : haystack) {
+ if (conversation.getContact() == contact) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
+ if (jid == null) {
+ return null;
+ }
+ for (final Conversation conversation : haystack) {
+ if ((account == null || conversation.getAccount() == account)
+ && (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
+ return this.findOrCreateConversation(account, jid, muc, null);
+ }
+
+ public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
+ synchronized (this.conversations) {
+ Conversation conversation = find(account, jid);
+ if (conversation != null) {
+ return conversation;
+ }
+ conversation = databaseBackend.findConversation(account, jid);
+ if (conversation != null) {
+ conversation.setStatus(Conversation.STATUS_AVAILABLE);
+ conversation.setAccount(account);
+ if (muc) {
+ conversation.setMode(Conversation.MODE_MULTI);
+ conversation.setContactJid(jid);
+ } else {
+ conversation.setMode(Conversation.MODE_SINGLE);
+ conversation.setContactJid(jid.toBareJid());
+ }
+ conversation.setNextEncryption(-1);
+ conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
+ this.databaseBackend.updateConversation(conversation);
+ } else {
+ String conversationName;
+ Contact contact = account.getRoster().getContact(jid);
+ if (contact != null) {
+ conversationName = contact.getDisplayName();
+ } else {
+ conversationName = jid.getLocalpart();
+ }
+ if (muc) {
+ conversation = new Conversation(conversationName, account, jid,
+ Conversation.MODE_MULTI);
+ } else {
+ conversation = new Conversation(conversationName, account, jid.toBareJid(),
+ Conversation.MODE_SINGLE);
+ }
+ this.databaseBackend.createConversation(conversation);
+ }
+ if (account.getXmppConnection() != null
+ && account.getXmppConnection().getFeatures().mam()
+ && !muc) {
+ if (query == null) {
+ this.mMessageArchiveService.query(conversation);
+ } else {
+ if (query.getConversation() == null) {
+ this.mMessageArchiveService.query(conversation, query.getStart());
+ }
+ }
+ }
+ checkDeletedFiles(conversation);
+ this.conversations.add(conversation);
+ updateConversationUi();
+ return conversation;
+ }
+ }
+
+ public void archiveConversation(Conversation conversation) {
+ conversation.setStatus(Conversation.STATUS_ARCHIVED);
+ conversation.setNextEncryption(-1);
+ synchronized (this.conversations) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null && bookmark.autojoin()) {
+ bookmark.setAutojoin(false);
+ pushBookmarks(bookmark.getAccount());
+ }
+ }
+ leaveMuc(conversation);
+ } else {
+ conversation.endOtrIfNeeded();
+ }
+ this.databaseBackend.updateConversation(conversation);
+ this.conversations.remove(conversation);
+ updateConversationUi();
+ }
+ }
+
+ public void createAccount(final Account account) {
+ account.initOtrEngine(this);
+ databaseBackend.createAccount(account);
+ this.accounts.add(account);
+ this.reconnectAccountInBackground(account);
+ updateAccountUi();
+ }
+
+ public void updateAccount(final Account account) {
+ this.statusListener.onStatusChanged(account);
+ databaseBackend.updateAccount(account);
+ reconnectAccount(account, false);
+ updateAccountUi();
+ getNotificationService().updateErrorNotification();
+ }
+
+ public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
+ final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
+ sendIqPacket(account, iq, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.setPassword(newPassword);
+ databaseBackend.updateAccount(account);
+ callback.onPasswordChangeSucceeded();
+ } else {
+ callback.onPasswordChangeFailed();
+ }
+ }
+ });
+ }
+
+ public void deleteAccount(final Account account) {
+ synchronized (this.conversations) {
+ for (final Conversation conversation : conversations) {
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation);
+ } else if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ conversation.endOtrIfNeeded();
+ }
+ conversations.remove(conversation);
+ }
+ }
+ if (account.getXmppConnection() != null) {
+ this.disconnect(account, true);
+ }
+ databaseBackend.deleteAccount(account);
+ this.accounts.remove(account);
+ updateAccountUi();
+ getNotificationService().updateErrorNotification();
+ }
+ }
+
+ public void setOnConversationListChangedListener(OnConversationUpdate listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnConversationUpdate = listener;
+ this.mNotificationService.setIsInForeground(true);
+ if (this.convChangedListenerCount < 2) {
+ this.convChangedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnConversationListChangedListener() {
+ synchronized (this) {
+ this.convChangedListenerCount--;
+ if (this.convChangedListenerCount <= 0) {
+ this.convChangedListenerCount = 0;
+ this.mOnConversationUpdate = null;
+ this.mNotificationService.setIsInForeground(false);
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnAccountListChangedListener(OnAccountUpdate listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnAccountUpdate = listener;
+ if (this.accountChangedListenerCount < 2) {
+ this.accountChangedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnAccountListChangedListener() {
+ synchronized (this) {
+ this.accountChangedListenerCount--;
+ if (this.accountChangedListenerCount <= 0) {
+ this.mOnAccountUpdate = null;
+ this.accountChangedListenerCount = 0;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnRosterUpdate = listener;
+ if (this.rosterChangedListenerCount < 2) {
+ this.rosterChangedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnRosterUpdateListener() {
+ synchronized (this) {
+ this.rosterChangedListenerCount--;
+ if (this.rosterChangedListenerCount <= 0) {
+ this.rosterChangedListenerCount = 0;
+ this.mOnRosterUpdate = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnUpdateBlocklist = listener;
+ if (this.updateBlocklistListenerCount < 2) {
+ this.updateBlocklistListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnUpdateBlocklistListener() {
+ synchronized (this) {
+ this.updateBlocklistListenerCount--;
+ if (this.updateBlocklistListenerCount <= 0) {
+ this.updateBlocklistListenerCount = 0;
+ this.mOnUpdateBlocklist = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnMucRosterUpdate = listener;
+ if (this.mucRosterChangedListenerCount < 2) {
+ this.mucRosterChangedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnMucRosterUpdateListener() {
+ synchronized (this) {
+ this.mucRosterChangedListenerCount--;
+ if (this.mucRosterChangedListenerCount <= 0) {
+ this.mucRosterChangedListenerCount = 0;
+ this.mOnMucRosterUpdate = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
+ private boolean checkListeners() {
+ return (this.mOnAccountUpdate == null
+ && this.mOnConversationUpdate == null
+ && this.mOnRosterUpdate == null
+ && this.mOnUpdateBlocklist == null);
+ }
+
+ private void switchToForeground() {
+ for (Account account : getAccounts()) {
+ if (account.getStatus() == Account.State.ONLINE) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null && connection.getFeatures().csi()) {
+ connection.sendActive();
+ }
+ }
+ }
+ Log.d(Config.LOGTAG, "app switched into foreground");
+ }
+
+ private void switchToBackground() {
+ for (Account account : getAccounts()) {
+ if (account.getStatus() == Account.State.ONLINE) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null && connection.getFeatures().csi()) {
+ connection.sendInactive();
+ }
+ }
+ }
+ for(Conversation conversation : getConversations()) {
+ conversation.setIncomingChatState(ChatState.ACTIVE);
+ }
+ this.mNotificationService.setIsInForeground(false);
+ Log.d(Config.LOGTAG, "app switched into background");
+ }
+
+ private void connectMultiModeConversations(Account account) {
+ List<Conversation> conversations = getConversations();
+ for (Conversation conversation : conversations) {
+ if ((conversation.getMode() == Conversation.MODE_MULTI)
+ && (conversation.getAccount() == account)) {
+ conversation.resetMucOptions();
+ joinMuc(conversation);
+ }
+ }
+ }
+
+ public void joinMuc(Conversation conversation) {
+ Account account = conversation.getAccount();
+ account.pendingConferenceJoins.remove(conversation);
+ account.pendingConferenceLeaves.remove(conversation);
+ if (account.getStatus() == Account.State.ONLINE) {
+ final String nick = conversation.getMucOptions().getProposedNick();
+ final Jid joinJid = conversation.getMucOptions().createJoinJid(nick);
+ if (joinJid == null) {
+ return; //safety net
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
+ PresencePacket packet = new PresencePacket();
+ packet.setFrom(conversation.getAccount().getJid());
+ packet.setTo(joinJid);
+ Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
+ if (conversation.getMucOptions().getPassword() != null) {
+ x.addChild("password").setContent(conversation.getMucOptions().getPassword());
+ }
+ x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("status").setContent("online");
+ packet.addChild("x", "jabber:x:signed").setContent(sig);
+ }
+ sendPresencePacket(account, packet);
+ fetchConferenceConfiguration(conversation);
+ if (!joinJid.equals(conversation.getJid())) {
+ conversation.setContactJid(joinJid);
+ databaseBackend.updateConversation(conversation);
+ }
+ conversation.setHasMessagesLeftOnServer(false);
+ } else {
+ account.pendingConferenceJoins.add(conversation);
+ }
+ }
+
+ public void providePasswordForMuc(Conversation conversation, String password) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ conversation.getMucOptions().setPassword(password);
+ if (conversation.getBookmark() != null) {
+ conversation.getBookmark().setAutojoin(true);
+ pushBookmarks(conversation.getAccount());
+ }
+ databaseBackend.updateConversation(conversation);
+ joinMuc(conversation);
+ }
+ }
+
+ public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
+ final MucOptions options = conversation.getMucOptions();
+ final Jid joinJid = options.createJoinJid(nick);
+ if (options.online()) {
+ Account account = conversation.getAccount();
+ options.setOnRenameListener(new OnRenameListener() {
+
+ @Override
+ public void onSuccess() {
+ conversation.setContactJid(joinJid);
+ databaseBackend.updateConversation(conversation);
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ bookmark.setNick(nick);
+ pushBookmarks(bookmark.getAccount());
+ }
+ callback.success(conversation);
+ }
+
+ @Override
+ public void onFailure() {
+ callback.error(R.string.nick_in_use, conversation);
+ }
+ });
+
+ PresencePacket packet = new PresencePacket();
+ packet.setTo(joinJid);
+ packet.setFrom(conversation.getAccount().getJid());
+
+ String sig = account.getPgpSignature();
+ if (sig != null) {
+ packet.addChild("status").setContent("online");
+ packet.addChild("x", "jabber:x:signed").setContent(sig);
+ }
+ sendPresencePacket(account, packet);
+ } else {
+ conversation.setContactJid(joinJid);
+ databaseBackend.updateConversation(conversation);
+ if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
+ Bookmark bookmark = conversation.getBookmark();
+ if (bookmark != null) {
+ bookmark.setNick(nick);
+ pushBookmarks(bookmark.getAccount());
+ }
+ joinMuc(conversation);
+ }
+ }
+ }
+
+ public void leaveMuc(Conversation conversation) {
+ Account account = conversation.getAccount();
+ account.pendingConferenceJoins.remove(conversation);
+ account.pendingConferenceLeaves.remove(conversation);
+ if (account.getStatus() == Account.State.ONLINE) {
+ PresencePacket packet = new PresencePacket();
+ packet.setTo(conversation.getJid());
+ packet.setFrom(conversation.getAccount().getJid());
+ packet.setAttribute("type", "unavailable");
+ sendPresencePacket(conversation.getAccount(), packet);
+ conversation.getMucOptions().setOffline();
+ conversation.deregisterWithBookmark();
+ Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
+ + ": leaving muc " + conversation.getJid());
+ } else {
+ account.pendingConferenceLeaves.add(conversation);
+ }
+ }
+
+ private String findConferenceServer(final Account account) {
+ String server;
+ if (account.getXmppConnection() != null) {
+ server = account.getXmppConnection().getMucServer();
+ if (server != null) {
+ return server;
+ }
+ }
+ for (Account other : getAccounts()) {
+ if (other != account && other.getXmppConnection() != null) {
+ server = other.getXmppConnection().getMucServer();
+ if (server != null) {
+ return server;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
+ if (account.getStatus() == Account.State.ONLINE) {
+ try {
+ String server = findConferenceServer(account);
+ if (server == null) {
+ if (callback != null) {
+ callback.error(R.string.no_conference_server_found, null);
+ }
+ return;
+ }
+ String name = new BigInteger(75, getRNG()).toString(32);
+ Jid jid = Jid.fromParts(name, server, null);
+ final Conversation conversation = findOrCreateConversation(account, jid, true);
+ joinMuc(conversation);
+ Bundle options = new Bundle();
+ options.putString("muc#roomconfig_persistentroom", "1");
+ options.putString("muc#roomconfig_membersonly", "1");
+ options.putString("muc#roomconfig_publicroom", "0");
+ options.putString("muc#roomconfig_whois", "anyone");
+ pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() {
+ @Override
+ public void onPushSucceeded() {
+ for (Jid invite : jids) {
+ invite(conversation, invite);
+ }
+ if (callback != null) {
+ callback.success(conversation);
+ }
+ }
+
+ @Override
+ public void onPushFailed() {
+ if (callback != null) {
+ callback.error(R.string.conference_creation_failed, conversation);
+ }
+ }
+ });
+
+ } catch (InvalidJidException e) {
+ if (callback != null) {
+ callback.error(R.string.conference_creation_failed, null);
+ }
+ }
+ } else {
+ if (callback != null) {
+ callback.error(R.string.not_connected_try_again, null);
+ }
+ }
+ }
+
+ public void fetchConferenceConfiguration(final Conversation conversation) {
+ IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+ request.setTo(conversation.getJid().toBareJid());
+ request.query("http://jabber.org/protocol/disco#info");
+ sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() != IqPacket.TYPE.ERROR) {
+ ArrayList<String> features = new ArrayList<>();
+ for (Element child : packet.query().getChildren()) {
+ if (child != null && child.getName().equals("feature")) {
+ String var = child.getAttribute("var");
+ if (var != null) {
+ features.add(var);
+ }
+ }
+ }
+ conversation.getMucOptions().updateFeatures(features);
+ updateConversationUi();
+ }
+ }
+ });
+ }
+
+ public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
+ IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+ request.setTo(conversation.getJid().toBareJid());
+ request.query("http://jabber.org/protocol/muc#owner");
+ sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() != IqPacket.TYPE.ERROR) {
+ Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
+ for (Field field : data.getFields()) {
+ if (options.containsKey(field.getName())) {
+ field.setValue(options.getString(field.getName()));
+ }
+ }
+ data.submit();
+ IqPacket set = new IqPacket(IqPacket.TYPE.SET);
+ set.setTo(conversation.getJid().toBareJid());
+ set.query("http://jabber.org/protocol/muc#owner").addChild(data);
+ sendIqPacket(account, set, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ if (callback != null) {
+ callback.onPushSucceeded();
+ }
+ } else {
+ if (callback != null) {
+ callback.onPushFailed();
+ }
+ }
+ }
+ });
+ } else {
+ if (callback != null) {
+ callback.onPushFailed();
+ }
+ }
+ }
+ });
+ }
+
+ public void pushSubjectToConference(final Conversation conference, final String subject) {
+ MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
+ this.sendMessagePacket(conference.getAccount(), packet);
+ final MucOptions mucOptions = conference.getMucOptions();
+ final MucOptions.User self = mucOptions.getSelf();
+ if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
+ Bundle options = new Bundle();
+ options.putString("muc#roomconfig_persistentroom", "1");
+ this.pushConferenceConfiguration(conference, options, null);
+ }
+ }
+
+ public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
+ final Jid jid = user.toBareJid();
+ IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
+ sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ callback.onAffiliationChangedSuccessful(jid);
+ } else {
+ callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
+ }
+ }
+ });
+ }
+
+ public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
+ List<Jid> jids = new ArrayList<>();
+ for (MucOptions.User user : conference.getMucOptions().getUsers()) {
+ if (user.getAffiliation() == before) {
+ jids.add(user.getJid());
+ }
+ }
+ IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
+ sendIqPacket(conference.getAccount(), request, null);
+ }
+
+ public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
+ IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
+ Log.d(Config.LOGTAG, request.toString());
+ sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Log.d(Config.LOGTAG, packet.toString());
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ callback.onRoleChangedSuccessful(nick);
+ } else {
+ callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
+ }
+ }
+ });
+ }
+
+ public void disconnect(Account account, boolean force) {
+ if ((account.getStatus() == Account.State.ONLINE)
+ || (account.getStatus() == Account.State.DISABLED)) {
+ if (!force) {
+ List<Conversation> conversations = getConversations();
+ for (Conversation conversation : conversations) {
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation);
+ } else {
+ if (conversation.endOtrIfNeeded()) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": ended otr session with "
+ + conversation.getJid());
+ }
+ }
+ }
+ }
+ }
+ account.getXmppConnection().disconnect(force);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public void updateMessage(Message message) {
+ databaseBackend.updateMessage(message);
+ updateConversationUi();
+ }
+
+ protected void syncDirtyContacts(Account account) {
+ for (Contact contact : account.getRoster().getContacts()) {
+ if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
+ pushContactToServer(contact);
+ }
+ if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
+ deleteContactOnServer(contact);
+ }
+ }
+ }
+
+ public void createContact(Contact contact) {
+ SharedPreferences sharedPref = getPreferences();
+ boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
+ if (autoGrant) {
+ contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+ contact.setOption(Contact.Options.ASKING);
+ }
+ pushContactToServer(contact);
+ }
+
+ public void onOtrSessionEstablished(Conversation conversation) {
+ final Account account = conversation.getAccount();
+ final Session otrSession = conversation.getOtrSession();
+ Log.d(Config.LOGTAG,
+ account.getJid().toBareJid() + " otr session established with "
+ + conversation.getJid() + "/"
+ + otrSession.getSessionID().getUserID());
+ conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
+
+ @Override
+ public void onMessageFound(Message message) {
+ SessionID id = otrSession.getSessionID();
+ try {
+ message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
+ } catch (InvalidJidException e) {
+ return;
+ }
+ if (message.getType() == Message.TYPE_TEXT) {
+ MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true);
+ if (outPacket != null) {
+ message.setStatus(Message.STATUS_SEND);
+ databaseBackend.updateMessage(message);
+ sendMessagePacket(account, outPacket);
+ }
+ } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
+ mJingleConnectionManager.createNewConnection(message);
+ }
+ updateConversationUi();
+ }
+ });
+ }
+
+ public boolean renewSymmetricKey(Conversation conversation) {
+ Account account = conversation.getAccount();
+ byte[] symmetricKey = new byte[32];
+ this.mRandom.nextBytes(symmetricKey);
+ Session otrSession = conversation.getOtrSession();
+ if (otrSession != null) {
+ MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_CHAT);
+ packet.setFrom(account.getJid());
+ packet.addChild("private", "urn:xmpp:carbons:2");
+ packet.addChild("no-copy", "urn:xmpp:hints");
+ packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
+ + otrSession.getSessionID().getUserID());
+ try {
+ packet.setBody(otrSession
+ .transformSending(CryptoHelper.FILETRANSFER
+ + CryptoHelper.bytesToHex(symmetricKey))[0]);
+ sendMessagePacket(account, packet);
+ conversation.setSymmetricKey(symmetricKey);
+ return true;
+ } catch (OtrException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public void pushContactToServer(final Contact contact) {
+ contact.resetOption(Contact.Options.DIRTY_DELETE);
+ contact.setOption(Contact.Options.DIRTY_PUSH);
+ final Account account = contact.getAccount();
+ if (account.getStatus() == Account.State.ONLINE) {
+ final boolean ask = contact.getOption(Contact.Options.ASKING);
+ final boolean sendUpdates = contact
+ .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
+ && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
+ final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ iq.query(Xmlns.ROSTER).addChild(contact.asElement());
+ account.getXmppConnection().sendIqPacket(iq, null);
+ if (sendUpdates) {
+ sendPresencePacket(account,
+ mPresenceGenerator.sendPresenceUpdatesTo(contact));
+ }
+ if (ask) {
+ sendPresencePacket(account,
+ mPresenceGenerator.requestPresenceUpdatesFrom(contact));
+ }
+ }
+ }
+
+ public void publishAvatar(final Account account,
+ final Uri image,
+ final UiCallback<Avatar> callback) {
+ final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
+ final int size = Config.AVATAR_SIZE;
+ final Avatar avatar = getFileBackend()
+ .getPepAvatar(image, size, format);
+ if (avatar != null) {
+ avatar.height = size;
+ avatar.width = size;
+ if (format.equals(Bitmap.CompressFormat.WEBP)) {
+ avatar.type = "image/webp";
+ } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
+ avatar.type = "image/jpeg";
+ } else if (format.equals(Bitmap.CompressFormat.PNG)) {
+ avatar.type = "image/png";
+ }
+ if (!getFileBackend().save(avatar)) {
+ callback.error(R.string.error_saving_avatar, avatar);
+ return;
+ }
+ final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
+ this.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket result) {
+ if (result.getType() == IqPacket.TYPE.RESULT) {
+ final IqPacket packet = XmppConnectionService.this.mIqGenerator
+ .publishAvatarMetadata(avatar);
+ sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account,
+ IqPacket result) {
+ if (result.getType() == IqPacket.TYPE.RESULT) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.updateAccount(account);
+ }
+ callback.success(avatar);
+ } else {
+ callback.error(
+ R.string.error_publish_avatar_server_reject,
+ avatar);
+ }
+ }
+ });
+ } else {
+ callback.error(
+ R.string.error_publish_avatar_server_reject,
+ avatar);
+ }
+ }
+ });
+ } else {
+ callback.error(R.string.error_publish_avatar_converting, null);
+ }
+ }
+
+ public void fetchAvatar(Account account, Avatar avatar) {
+ fetchAvatar(account, avatar, null);
+ }
+
+ public void fetchAvatar(Account account, final Avatar avatar,
+ final UiCallback<Avatar> callback) {
+ IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
+ sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket result) {
+ final String ERROR = account.getJid().toBareJid()
+ + ": fetching avatar for " + avatar.owner + " failed ";
+ if (result.getType() == IqPacket.TYPE.RESULT) {
+ avatar.image = mIqParser.avatarData(result);
+ if (avatar.image != null) {
+ if (getFileBackend().save(avatar)) {
+ if (account.getJid().toBareJid().equals(avatar.owner)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.updateAccount(account);
+ }
+ getAvatarService().clear(account);
+ updateConversationUi();
+ updateAccountUi();
+ } else {
+ Contact contact = account.getRoster()
+ .getContact(avatar.owner);
+ contact.setAvatar(avatar.getFilename());
+ getAvatarService().clear(contact);
+ updateConversationUi();
+ updateRosterUi();
+ }
+ if (callback != null) {
+ callback.success(avatar);
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ + ": succesfully fetched avatar for "
+ + avatar.owner);
+ return;
+ }
+ } else {
+
+ Log.d(Config.LOGTAG, ERROR + "(parsing error)");
+ }
+ } else {
+ Element error = result.findChild("error");
+ if (error == null) {
+ Log.d(Config.LOGTAG, ERROR + "(server error)");
+ } else {
+ Log.d(Config.LOGTAG, ERROR + error.toString());
+ }
+ }
+ if (callback != null) {
+ callback.error(0, null);
+ }
+
+ }
+ });
+ }
+
+ public void checkForAvatar(Account account,
+ final UiCallback<Avatar> callback) {
+ IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
+ this.sendIqPacket(account, packet, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Element pubsub = packet.findChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ if (pubsub != null) {
+ Element items = pubsub.findChild("items");
+ if (items != null) {
+ Avatar avatar = Avatar.parseMetadata(items);
+ if (avatar != null) {
+ avatar.owner = account.getJid().toBareJid();
+ if (fileBackend.isAvatarCached(avatar)) {
+ if (account.setAvatar(avatar.getFilename())) {
+ databaseBackend.updateAccount(account);
+ }
+ getAvatarService().clear(account);
+ callback.success(avatar);
+ } else {
+ fetchAvatar(account, avatar, callback);
+ }
+ return;
+ }
+ }
+ }
+ }
+ callback.error(0, null);
+ }
+ });
+ }
+
+ public void deleteContactOnServer(Contact contact) {
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ contact.resetOption(Contact.Options.DIRTY_PUSH);
+ contact.setOption(Contact.Options.DIRTY_DELETE);
+ Account account = contact.getAccount();
+ if (account.getStatus() == Account.State.ONLINE) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
+ Element item = iq.query(Xmlns.ROSTER).addChild("item");
+ item.setAttribute("jid", contact.getJid().toString());
+ item.setAttribute("subscription", "remove");
+ account.getXmppConnection().sendIqPacket(iq, null);
+ }
+ }
+
+ public void updateConversation(Conversation conversation) {
+ this.databaseBackend.updateConversation(conversation);
+ }
+
+ public void reconnectAccount(final Account account, final boolean force) {
+ synchronized (account) {
+ if (account.getXmppConnection() != null) {
+ disconnect(account, force);
+ }
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ if (account.getXmppConnection() == null) {
+ account.setXmppConnection(createConnection(account));
+ }
+ Thread thread = new Thread(account.getXmppConnection());
+ thread.start();
+ scheduleWakeUpCall(Config.CONNECT_TIMEOUT, account.getUuid().hashCode());
+ } else {
+ account.getRoster().clearPresences();
+ account.setXmppConnection(null);
+ }
+ }
+ }
+
+ public void reconnectAccountInBackground(final Account account) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ reconnectAccount(account,false);
+ }
+ }).start();
+ }
+
+ public void invite(Conversation conversation, Jid contact) {
+ MessagePacket packet = mMessageGenerator.invite(conversation, contact);
+ sendMessagePacket(conversation.getAccount(), packet);
+ }
+
+ public void resetSendingToWaiting(Account account) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getAccount() == account) {
+ conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
+
+ @Override
+ public void onMessageFound(Message message) {
+ markMessage(message, Message.STATUS_WAITING);
+ }
+ });
+ }
+ }
+ }
+
+ public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
+ if (uuid == null) {
+ return null;
+ }
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
+ final Message message = conversation.findSentMessageWithUuid(uuid);
+ if (message != null) {
+ markMessage(message, status);
+ }
+ return message;
+ }
+ }
+ return null;
+ }
+
+ public boolean markMessage(Conversation conversation, String uuid,
+ int status) {
+ if (uuid == null) {
+ return false;
+ } else {
+ Message message = conversation.findSentMessageWithUuid(uuid);
+ if (message != null) {
+ markMessage(message, status);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ public void markMessage(Message message, int status) {
+ if (status == Message.STATUS_SEND_FAILED
+ && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
+ .getStatus() == Message.STATUS_SEND_DISPLAYED)) {
+ return;
+ }
+ message.setStatus(status);
+ databaseBackend.updateMessage(message);
+ updateConversationUi();
+ }
+
+ public SharedPreferences getPreferences() {
+ return PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+ }
+
+ public boolean forceEncryption() {
+ return getPreferences().getBoolean("force_encryption", false);
+ }
+
+ public boolean sendChatStates() {
+ return getPreferences().getBoolean("chat_states", false);
+ }
+
+ public boolean saveEncryptedMessages() {
+ return !getPreferences().getBoolean("dont_save_encrypted", false);
+ }
+
+ public boolean indicateReceived() {
+ return getPreferences().getBoolean("indicate_received", false);
+ }
+
+ public int unreadCount() {
+ int count = 0;
+ for(Conversation conversation : getConversations()) {
+ count += conversation.unreadCount();
+ }
+ return count;
+ }
+
+ public void updateConversationUi() {
+ if (mOnConversationUpdate != null) {
+ mOnConversationUpdate.onConversationUpdate();
+ }
+ }
+
+ public void updateAccountUi() {
+ if (mOnAccountUpdate != null) {
+ mOnAccountUpdate.onAccountUpdate();
+ }
+ }
+
+ public void updateRosterUi() {
+ if (mOnRosterUpdate != null) {
+ mOnRosterUpdate.onRosterUpdate();
+ }
+ }
+
+ public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
+ if (mOnUpdateBlocklist != null) {
+ mOnUpdateBlocklist.OnUpdateBlocklist(status);
+ }
+ }
+
+ public void updateMucRosterUi() {
+ if (mOnMucRosterUpdate != null) {
+ mOnMucRosterUpdate.onMucRosterUpdate();
+ }
+ }
+
+ public Account findAccountByJid(final Jid accountJid) {
+ for (Account account : this.accounts) {
+ if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ public Conversation findConversationByUuid(String uuid) {
+ for (Conversation conversation : getConversations()) {
+ if (conversation.getUuid().equals(uuid)) {
+ return conversation;
+ }
+ }
+ return null;
+ }
+
+ public void markRead(final Conversation conversation) {
+ mNotificationService.clear(conversation);
+ conversation.markRead();
+ }
+
+ public void sendReadMarker(final Conversation conversation) {
+ final Message markable = conversation.getLatestMarkableMessage();
+ this.markRead(conversation);
+ if (Settings.CONFIRM_MESSAGE_READ && markable != null && markable.getRemoteMsgId() != null) {
+ Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
+ Account account = conversation.getAccount();
+ final Jid to = markable.getCounterpart();
+ MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
+ this.sendMessagePacket(conversation.getAccount(), packet);
+ }
+ updateConversationUi();
+ }
+
+ public SecureRandom getRNG() {
+ return this.mRandom;
+ }
+
+ public MemorizingTrustManager getMemorizingTrustManager() {
+ return this.mMemorizingTrustManager;
+ }
+
+ public PowerManager getPowerManager() {
+ return this.pm;
+ }
+
+ public LruCache<String, Bitmap> getBitmapCache() {
+ return this.mBitmapCache;
+ }
+
+ public void syncRosterToDisk(final Account account) {
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ databaseBackend.writeRoster(account.getRoster());
+ }
+ }).start();
+
+ }
+
+ public List<String> getKnownHosts() {
+ final List<String> hosts = new ArrayList<>();
+ for (final Account account : getAccounts()) {
+ if (!hosts.contains(account.getServer().toString())) {
+ hosts.add(account.getServer().toString());
+ }
+ for (final Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster()) {
+ final String server = contact.getServer().toString();
+ if (server != null && !hosts.contains(server)) {
+ hosts.add(server);
+ }
+ }
+ }
+ }
+ return hosts;
+ }
+
+ public List<String> getKnownConferenceHosts() {
+ final ArrayList<String> mucServers = new ArrayList<>();
+ for (final Account account : accounts) {
+ if (account.getXmppConnection() != null) {
+ final String server = account.getXmppConnection().getMucServer();
+ if (server != null && !mucServers.contains(server)) {
+ mucServers.add(server);
+ }
+ }
+ }
+ return mucServers;
+ }
+
+ public void sendMessagePacket(Account account, MessagePacket packet) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendMessagePacket(packet);
+ }
+ }
+
+ public void sendPresencePacket(Account account, PresencePacket packet) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendPresencePacket(packet);
+ }
+ }
+
+ public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
+ final XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendIqPacket(packet, callback);
+ }
+ }
+
+ public void sendPresence(final Account account) {
+ sendPresencePacket(account, mPresenceGenerator.sendPresence(account));
+ }
+
+ public MessageGenerator getMessageGenerator() {
+ return this.mMessageGenerator;
+ }
+
+ public PresenceGenerator getPresenceGenerator() {
+ return this.mPresenceGenerator;
+ }
+
+ public IqGenerator getIqGenerator() {
+ return this.mIqGenerator;
+ }
+
+ public IqParser getIqParser() {
+ return this.mIqParser;
+ }
+
+ public JingleConnectionManager getJingleConnectionManager() {
+ return this.mJingleConnectionManager;
+ }
+
+ public MessageArchiveService getMessageArchiveService() {
+ return this.mMessageArchiveService;
+ }
+
+ public List<Contact> findContacts(Jid jid) {
+ ArrayList<Contact> contacts = new ArrayList<>();
+ for (Account account : getAccounts()) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ Contact contact = account.getRoster().getContactFromRoster(jid);
+ if (contact != null) {
+ contacts.add(contact);
+ }
+ }
+ }
+ return contacts;
+ }
+
+ public NotificationService getNotificationService() {
+ return this.mNotificationService;
+ }
+
+ public HttpConnectionManager getHttpConnectionManager() {
+ return this.mHttpConnectionManager;
+ }
+
+ public void resendFailedMessages(final Message message) {
+ final Collection<Message> messages = new ArrayList<>();
+ Message current = message;
+ while (current.getStatus() == Message.STATUS_SEND_FAILED) {
+ messages.add(current);
+ if (current.mergeable(current.next())) {
+ current = current.next();
+ } else {
+ break;
+ }
+ }
+ for (final Message msg : messages) {
+ markMessage(msg, Message.STATUS_WAITING);
+ this.resendMessage(msg);
+ }
+ }
+
+ public void clearConversationHistory(final Conversation conversation) {
+ conversation.clearMessages();
+ conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ databaseBackend.deleteMessagesInConversation(conversation);
+ }
+ }).start();
+ }
+
+ public void sendBlockRequest(final Blockable blockable) {
+ if (blockable != null && blockable.getBlockedJid() != null) {
+ final Jid jid = blockable.getBlockedJid();
+ this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.getBlocklist().add(jid);
+ updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
+ }
+ }
+ });
+ }
+ }
+
+ public void sendUnblockRequest(final Blockable blockable) {
+ if (blockable != null && blockable.getJid() != null) {
+ final Jid jid = blockable.getBlockedJid();
+ this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(final Account account, final IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ account.getBlocklist().remove(jid);
+ updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
+ }
+ }
+ });
+ }
+ }
+
+ public interface OnMoreMessagesLoaded {
+ public void onMoreMessagesLoaded(int count, Conversation conversation);
+
+ public void informUser(int r);
+ }
+
+ public interface OnAccountPasswordChanged {
+ public void onPasswordChangeSucceeded();
+
+ public void onPasswordChangeFailed();
+ }
+
+ public interface OnAffiliationChanged {
+ public void onAffiliationChangedSuccessful(Jid jid);
+
+ public void onAffiliationChangeFailed(Jid jid, int resId);
+ }
+
+ public interface OnRoleChanged {
+ public void onRoleChangedSuccessful(String nick);
+
+ public void onRoleChangeFailed(String nick, int resid);
+ }
+
+ public interface OnConversationUpdate {
+ public void onConversationUpdate();
+ }
+
+ public interface OnAccountUpdate {
+ public void onAccountUpdate();
+ }
+
+ public interface OnRosterUpdate {
+ public void onRosterUpdate();
+ }
+
+ public interface OnMucRosterUpdate {
+ public void onMucRosterUpdate();
+ }
+
+ public interface OnConferenceOptionsPushed {
+ public void onPushSucceeded();
+
+ public void onPushFailed();
+ }
+
+ public class XmppConnectionBinder extends Binder {
+ public XmppConnectionService getService() {
+ return XmppConnectionService.this;
+ }
+ }
+}