diff options
Diffstat (limited to '')
8 files changed, 1678 insertions, 525 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java index 6936c635..7c937475 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/AbstractConnectionManager.java @@ -1,6 +1,37 @@ package de.thedevstack.conversationsplus.services; -import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.PowerManager; +import android.util.Log; +import android.util.Pair; + +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.DownloadableFile; public class AbstractConnectionManager { protected XmppConnectionService mXmppConnectionService; @@ -12,4 +43,91 @@ public class AbstractConnectionManager { public XmppConnectionService getXmppConnectionService() { return this.mXmppConnectionService; } + + public boolean hasStoragePermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return mXmppConnectionService.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } else { + return true; + } + } + + public static Pair<InputStream,Integer> createInputStream(DownloadableFile file, boolean gcm) throws FileNotFoundException { + FileInputStream is; + int size; + is = new FileInputStream(file); + size = (int) file.getSize(); + if (file.getKey() == null) { + return new Pair<InputStream,Integer>(is,size); + } + try { + if (gcm) { + AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); + InputStream cis = new org.bouncycastle.crypto.io.CipherInputStream(is, cipher); + return new Pair<>(cis, cipher.getOutputSize(size)); + } else { + IvParameterSpec ips = new IvParameterSpec(file.getIv()); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips); + Log.d(Config.LOGTAG, "opening encrypted input stream"); + final int s = Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE ? size : (size / 16 + 1) * 16; + return new Pair<InputStream,Integer>(new CipherInputStream(is, cipher),s); + } + } catch (InvalidKeyException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (NoSuchPaddingException e) { + return null; + } catch (InvalidAlgorithmParameterException e) { + return null; + } + } + + public static OutputStream createAppendedOutputStream(DownloadableFile file) { + return createOutputStream(file, false, true); + } + + public static OutputStream createOutputStream(DownloadableFile file, boolean gcm) { + return createOutputStream(file, gcm, false); + } + + private static OutputStream createOutputStream(DownloadableFile file, boolean gcm, boolean append) { + FileOutputStream os; + try { + os = new FileOutputStream(file, append); + if (file.getKey() == null) { + return os; + } + } catch (FileNotFoundException e) { + return null; + } + try { + if (gcm) { + AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); + return new org.bouncycastle.crypto.io.CipherOutputStream(os, cipher); + } else { + IvParameterSpec ips = new IvParameterSpec(file.getIv()); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips); + Log.d(Config.LOGTAG, "opening encrypted output stream"); + return new CipherOutputStream(os, cipher); + } + } catch (InvalidKeyException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (NoSuchPaddingException e) { + return null; + } catch (InvalidAlgorithmParameterException e) { + return null; + } + } + + public PowerManager.WakeLock createWakeLock(String name) { + PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE); + return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name); + } } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java b/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java index 1cbda222..11ff36da 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java @@ -13,27 +13,28 @@ import java.util.List; import java.util.Locale; import de.thedevstack.android.logcat.Logging; -import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.utils.AvatarUtil; +import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; +import de.thedevstack.conversationsplus.utils.XmppSendUtil; +import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketGenerator; +import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketParser; +import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; 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.Message; import de.thedevstack.conversationsplus.entities.MucOptions; import de.thedevstack.conversationsplus.generator.IqGenerator; import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import de.thedevstack.conversationsplus.ui.UiCallback; -import de.thedevstack.conversationsplus.utils.AvatarUtil; -import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.utils.UIHelper; -import de.thedevstack.conversationsplus.utils.UiUpdateHelper; -import de.thedevstack.conversationsplus.utils.XmppSendUtil; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; -import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketGenerator; -import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketParser; import de.thedevstack.conversationsplus.xmpp.pep.Avatar; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; @@ -75,6 +76,36 @@ public class AvatarService { return avatar; } + public Bitmap get(final MucOptions.User user, final int size, boolean cachedOnly) { + Contact c = user.getContact(); + if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) { + return get(c, size, cachedOnly); + } else { + return getImpl(user, size, cachedOnly); + } + } + + private Bitmap getImpl(final MucOptions.User user, final int size, boolean cachedOnly) { + final String KEY = key(user, size); + Bitmap avatar = ImageUtil.getBitmapFromCache(KEY); + if (avatar != null || cachedOnly) { + return avatar; + } + if (user.getAvatar() != null) { + avatar = AvatarUtil.getAvatar(user.getAvatar(), size); + } + if (avatar == null) { + Contact contact = user.getContact(); + if (contact != null) { + avatar = get(contact, size, cachedOnly); + } else { + avatar = get(user.getName(), size, cachedOnly); + } + } + ImageUtil.addBitmapToCache(KEY, avatar); + return avatar; + } + public void clear(Contact contact) { synchronized (this.sizes) { for (Integer size : sizes) { @@ -93,6 +124,16 @@ public class AvatarService { + contact.getJid() + "_" + String.valueOf(size); } + private String key(MucOptions.User user, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_CONTACT + "_" + user.getAccount().getJid().toBareJid() + "_" + + user.getFullJid() + "_" + String.valueOf(size); + } + public Bitmap get(ListItem item, int size) { return get(item,size,false); } @@ -138,7 +179,7 @@ public class AvatarService { if (bitmap != null || cachedOnly) { return bitmap; } - final List<MucOptions.User> users = new ArrayList<>(mucOptions.getUsers()); + final List<MucOptions.User> users = mucOptions.getUsers(); int count = users.size(); bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); @@ -146,11 +187,10 @@ public class AvatarService { 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); + drawTile(canvas, name, 0, 0, size, size); } else if (count == 1) { - drawTile(canvas, users.get(0), 0, 0, size, size); + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); + drawTile(canvas, mucOptions.getConversation().getAccount(), size / 2 + 1, 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); @@ -195,9 +235,13 @@ public class AvatarService { } public Bitmap get(Account account, int size) { + return get(account, size, false); + } + + public Bitmap get(Account account, int size, boolean cachedOnly) { final String KEY = key(account, size); Bitmap avatar = ImageUtil.getBitmapFromCache(KEY); - if (avatar != null) { + if (avatar != null || cachedOnly) { return avatar; } avatar = AvatarUtil.getAvatar(account.getAvatar(), size); @@ -208,6 +252,24 @@ public class AvatarService { return avatar; } + public Bitmap get(Message message, int size, boolean cachedOnly) { + final Conversation conversation = message.getConversation(); + if (message.getStatus() == Message.STATUS_RECEIVED) { + Contact c = message.getContact(); + if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) { + return get(c, size, cachedOnly); + } else if (message.getConversation().getMode() == Conversation.MODE_MULTI){ + MucOptions.User user = conversation.getMucOptions().findUser(message.getCounterpart().getResourcepart()); + if (user != null) { + return getImpl(user,size,cachedOnly); + } + } + return get(UIHelper.getMessageDisplayName(message), size, cachedOnly); + } else { + return get(conversation.getAccount(), size, cachedOnly); + } + } + public void clear(Account account) { synchronized (this.sizes) { for (Integer size : sizes) { @@ -216,6 +278,14 @@ public class AvatarService { } } + public void clear(MucOptions.User user) { + synchronized (this.sizes) { + for (Integer size : sizes) { + ImageUtil.removeBitmapFromCache(key(user, size)); + } + } + } + private String key(Account account, int size) { synchronized (this.sizes) { if (!this.sizes.contains(size)) { @@ -239,9 +309,7 @@ public class AvatarService { 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); + drawTile(canvas, trimmedName, 0, 0, size, size); ImageUtil.addBitmapToCache(KEY, bitmap); return bitmap; } @@ -255,7 +323,7 @@ public class AvatarService { return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size); } - private void drawTile(Canvas canvas, String letter, int tileColor, + private boolean 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(); @@ -271,10 +339,11 @@ public class AvatarService { 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); + / 2 + rect.height() / 2, textPaint); + return true; } - private void drawTile(Canvas canvas, MucOptions.User user, int left, + private boolean drawTile(Canvas canvas, MucOptions.User user, int left, int top, int right, int bottom) { Contact contact = user.getContact(); if (contact != null) { @@ -284,24 +353,59 @@ public class AvatarService { } else if (contact.getAvatar() != null) { uri = AvatarUtil.getAvatarUri(contact.getAvatar()); } + if (drawTile(canvas, uri, left, top, right, bottom)) { + return true; + } + } else if (user.getAvatar() != null) { + Uri uri = AvatarUtil.getAvatarUri(user.getAvatar()); + if (drawTile(canvas, uri, left, top, right, bottom)) { + return true; + } + } + String name = contact != null ? contact.getDisplayName() : user.getName(); + drawTile(canvas, name, left, top, right, bottom); + return true; + } + + private boolean drawTile(Canvas canvas, Account account, int left, int top, int right, int bottom) { + String avatar = account.getAvatar(); + if (avatar != null) { + Uri uri = AvatarUtil.getAvatarUri(avatar); if (uri != null) { - Bitmap bitmap = ImageUtil.cropCenter(uri, bottom - top, right - left); - if (bitmap != null) { - drawTile(canvas, bitmap, left, top, right, bottom); - return; + if (drawTile(canvas, uri, left, top, right, bottom)) { + return true; } } } - 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); + return drawTile(canvas, account.getJid().toBareJid().toString(), left, top, right, bottom); + } + + private boolean drawTile(Canvas canvas, String name, int left, int top, int right, int bottom) { + if (name != null) { + final String letter = name.isEmpty() ? "X" : name.substring(0, 1); + final int color = UIHelper.getColorForName(name); + drawTile(canvas, letter, color, left, top, right, bottom); + return true; + } + return false; + } + + private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) { + if (uri != null) { + Bitmap bitmap = ImageUtil.cropCenter(uri, bottom - top, right - left); + if (bitmap != null) { + drawTile(canvas, bitmap, left, top, right, bottom); + return true; + } + } + return false; } - private void drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop, + private boolean 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); + return true; } public void publishAvatar(final Account account, diff --git a/src/main/java/de/thedevstack/conversationsplus/services/ContactChooserTargetService.java b/src/main/java/de/thedevstack/conversationsplus/services/ContactChooserTargetService.java new file mode 100644 index 00000000..8d48d686 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/ContactChooserTargetService.java @@ -0,0 +1,83 @@ +package de.thedevstack.conversationsplus.services; + +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.service.chooser.ChooserTarget; +import android.service.chooser.ChooserTargetService; + +import java.util.ArrayList; +import java.util.List; + +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.ui.ShareWithActivity; + +@TargetApi(Build.VERSION_CODES.M) +public class ContactChooserTargetService extends ChooserTargetService implements ServiceConnection { + + private final Object lock = new Object(); + + private XmppConnectionService mXmppConnectionService; + + private final int MAX_TARGETS = 5; + + @Override + public List<ChooserTarget> onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) { + Intent intent = new Intent(this, XmppConnectionService.class); + intent.setAction("contact_chooser"); + startService(intent); + bindService(intent, this, Context.BIND_AUTO_CREATE); + ArrayList<ChooserTarget> chooserTargets = new ArrayList<>(); + try { + waitForService(); + final ArrayList<Conversation> conversations = new ArrayList<>(); + if (!mXmppConnectionService.areMessagesInitialized()) { + return chooserTargets; + } + mXmppConnectionService.populateWithOrderedConversations(conversations, false); + final ComponentName componentName = new ComponentName(this, ShareWithActivity.class); + final int pixel = (int) (48 * getResources().getDisplayMetrics().density); + for(int i = 0; i < Math.min(conversations.size(),MAX_TARGETS); ++i) { + final Conversation conversation = conversations.get(i); + final String name = conversation.getName(); + final Icon icon = Icon.createWithBitmap(AvatarService.getInstance().get(conversation, pixel)); + final float score = (1.0f / MAX_TARGETS) * i; + final Bundle extras = new Bundle(); + extras.putString("uuid", conversation.getUuid()); + chooserTargets.add(new ChooserTarget(name, icon, score, componentName, extras)); + } + } catch (InterruptedException e) { + } + unbindService(this); + return chooserTargets; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service; + mXmppConnectionService = binder.getService(); + synchronized (this.lock) { + lock.notifyAll(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mXmppConnectionService = null; + } + + private void waitForService() throws InterruptedException { + if (mXmppConnectionService == null) { + synchronized (this.lock) { + lock.wait(); + } + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java b/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java index 4367dd1b..85e92552 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/EventReceiver.java @@ -1,10 +1,11 @@ package de.thedevstack.conversationsplus.services; -import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import de.thedevstack.conversationsplus.persistance.DatabaseBackend; + public class EventReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { diff --git a/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java b/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java new file mode 100644 index 00000000..0b898125 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java @@ -0,0 +1,147 @@ +package de.thedevstack.conversationsplus.services; + +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +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.persistance.DatabaseBackend; +import de.thedevstack.conversationsplus.persistance.FileBackend; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class ExportLogsService extends Service { + + private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + private static final String DIRECTORY_STRING_FORMAT = FileBackend.getConversationsFileDirectory() + "/logs/%s"; + private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n"; + private static final int NOTIFICATION_ID = 1; + private static AtomicBoolean running = new AtomicBoolean(false); + private DatabaseBackend mDatabaseBackend; + private List<Account> mAccounts; + + @Override + public void onCreate() { + mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext()); + mAccounts = mDatabaseBackend.getAccounts(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (running.compareAndSet(false, true)) { + new Thread(new Runnable() { + @Override + public void run() { + running.set(false); + export(); + stopForeground(true); + stopSelf(); + } + }).start(); + } + return START_NOT_STICKY; + } + + private void export() { + List<Conversation> conversations = mDatabaseBackend.getConversations(Conversation.STATUS_AVAILABLE); + conversations.addAll(mDatabaseBackend.getConversations(Conversation.STATUS_ARCHIVED)); + NotificationManager mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext()); + mBuilder.setContentTitle(getString(R.string.notification_export_logs_title)) + .setSmallIcon(R.drawable.ic_import_export_white_24dp) + .setProgress(conversations.size(), 0, false); + startForeground(NOTIFICATION_ID, mBuilder.build()); + + int progress = 0; + for (Conversation conversation : conversations) { + writeToFile(conversation); + progress++; + mBuilder.setProgress(conversations.size(), progress, false); + mNotifyManager.notify(NOTIFICATION_ID, mBuilder.build()); + } + } + + private void writeToFile(Conversation conversation) { + Jid accountJid = resolveAccountUuid(conversation.getAccountUuid()); + Jid contactJid = conversation.getJid(); + + File dir = new File(String.format(DIRECTORY_STRING_FORMAT,accountJid.toBareJid().toString())); + dir.mkdirs(); + + BufferedWriter bw = null; + try { + for (Message message : mDatabaseBackend.getMessagesIterable(conversation)) { + if (message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) { + String date = simpleDateFormat.format(new Date(message.getTimeSent())); + if (bw == null) { + bw = new BufferedWriter(new FileWriter( + new File(dir, contactJid.toBareJid().toString() + ".txt"))); + } + String jid = null; + switch (message.getStatus()) { + case Message.STATUS_RECEIVED: + jid = getMessageCounterpart(message); + break; + case Message.STATUS_SEND: + case Message.STATUS_SEND_RECEIVED: + case Message.STATUS_SEND_DISPLAYED: + jid = accountJid.toBareJid().toString(); + break; + } + if (jid != null) { + String body = message.hasFileOnRemoteHost() ? message.getFileParams().url.toString() : message.getBody(); + bw.write(String.format(MESSAGE_STRING_FORMAT, date, jid, + body.replace("\\\n", "\\ \n").replace("\n", "\\ \n"))); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (bw != null) { + bw.close(); + } + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + + private Jid resolveAccountUuid(String accountUuid) { + for (Account account : mAccounts) { + if (account.getUuid().equals(accountUuid)) { + return account.getJid(); + } + } + return null; + } + + private String getMessageCounterpart(Message message) { + String trueCounterpart = (String) message.getContentValues().get(Message.TRUE_COUNTERPART); + if (trueCounterpart != null) { + return trueCounterpart; + } else { + return message.getCounterpart().toString(); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } +}
\ No newline at end of file diff --git a/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java b/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java index 1e0abec1..a61ade40 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/MessageArchiveService.java @@ -1,5 +1,7 @@ package de.thedevstack.conversationsplus.services; +import android.util.Pair; + import java.math.BigInteger; import java.util.ArrayList; import java.util.HashSet; @@ -22,8 +24,8 @@ 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>(); + private final HashSet<Query> queries = new HashSet<>(); + private final ArrayList<Query> pendingQueries = new ArrayList<>(); public enum PagingOrder { NORMAL, @@ -34,7 +36,15 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { this.mXmppConnectionService = service; } - public void catchup(final Account account) { + private void catchup(final Account account) { + synchronized (this.queries) { + for(Iterator<Query> iterator = this.queries.iterator(); iterator.hasNext();) { + Query query = iterator.next(); + if (query.getAccount() == account) { + iterator.remove(); + } + } + } long startCatchup = getLastMessageTransmitted(account); long endCatchup = account.getXmppConnection().getLastSessionEstablished(); if (startCatchup == 0) { @@ -53,21 +63,33 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { 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; - } - } + public void catchupMUC(final Conversation conversation) { + if (conversation.getLastMessageTransmitted() < 0 && conversation.countMessages() == 0) { + query(conversation, + 0, + System.currentTimeMillis()); + } else { + query(conversation, + conversation.getLastMessageTransmitted(), + System.currentTimeMillis()); } - return timestamp; + } + + private long getLastMessageTransmitted(final Account account) { + Pair<Long,String> pair = mXmppConnectionService.databaseBackend.getLastMessageReceived(account); + return pair == null ? 0 : pair.first; } public Query query(final Conversation conversation) { - return query(conversation,conversation.getAccount().getXmppConnection().getLastSessionEstablished()); + if (conversation.getLastMessageTransmitted() < 0 && conversation.countMessages() == 0) { + return query(conversation, + 0, + System.currentTimeMillis()); + } else { + return query(conversation, + conversation.getLastMessageTransmitted(), + conversation.getAccount().getXmppConnection().getLastSessionEstablished()); + } } public Query query(final Conversation conversation, long end) { @@ -80,6 +102,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return null; } final Query query = new Query(conversation, start, end,PagingOrder.REVERSE); + query.reference = conversation.getFirstMamReference(); this.queries.add(query); this.execute(query); return query; @@ -110,9 +133,16 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.ERROR) { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + synchronized (MessageArchiveService.this.queries) { + MessageArchiveService.this.queries.remove(query); + if (query.hasCallback()) { + query.callback(false); + } + } + } else if (packet.getType() != IqPacket.TYPE.RESULT) { Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString()); - finalizeQuery(query); + finalizeQuery(query, true); } } }); @@ -123,32 +153,29 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - private void finalizeQuery(Query query) { + private void finalizeQuery(Query query, boolean done) { 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); - } - if (query.hasCallback()) { - query.callback(); - } else { - conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0); - this.mXmppConnectionService.updateConversationUi(); - } + conversation.setHasMessagesLeftOnServer(!done); } else { for(Conversation tmp : this.mXmppConnectionService.getConversations()) { if (tmp.getAccount() == query.getAccount()) { tmp.sort(); - if (tmp.setLastMessageTransmitted(query.getEnd())) { - this.mXmppConnectionService.databaseBackend.updateConversation(tmp); - } } } } + if (query.hasCallback()) { + query.callback(done); + } else { + if (null != conversation) { + conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0); + } + this.mXmppConnectionService.updateConversationUi(); + } } public boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) { @@ -165,6 +192,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } + public boolean queryInProgress(Conversation conversation) { + return queryInProgress(conversation, null); + } + public void processFin(Element fin, Jid from) { if (fin == null) { return; @@ -179,9 +210,16 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { 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 (query.getConversation() != null) { + query.getConversation().setFirstMamReference(first == null ? null : first.getContent()); + } if (complete || relevant == null || abort) { - this.finalizeQuery(query); - Logging.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages"); + final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() == 0; + this.finalizeQuery(query, done); + Logging.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done)); + if (query.getWith() == null && query.getMessageCount() > 0) { + mXmppConnectionService.getNotificationService().finishBacklog(true); + } } else { final Query nextQuery; if (query.getPagingOrder() == PagingOrder.NORMAL) { @@ -190,9 +228,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { nextQuery = query.prev(first == null ? null : first.getContent()); } this.execute(nextQuery); - this.finalizeQuery(query); + this.finalizeQuery(query, false); synchronized (this.queries) { - this.queries.remove(query); this.queries.add(nextQuery); } } @@ -298,10 +335,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { this.callback = callback; } - public void callback() { + public void callback(boolean done) { if (this.callback != null) { this.callback.onMoreMessagesLoaded(messageCount,conversation); - if (messageCount == 0) { + if (done) { this.callback.informUser(R.string.no_more_history_on_server); } } @@ -319,12 +356,9 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return this.account; } - public void incrementTotalCount() { - this.totalCount++; - } - public void incrementMessageCount() { this.messageCount++; + this.totalCount++; } public int getTotalCount() { @@ -347,7 +381,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { public String toString() { StringBuilder builder = new StringBuilder(); if (this.muc()) { - builder.append("to="+this.getWith().toString()); + builder.append("to="); + builder.append(this.getWith().toString()); } else { builder.append("with="); if (this.getWith() == null) { diff --git a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java index b02b4d82..92b7cfa1 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java @@ -1,6 +1,5 @@ package de.thedevstack.conversationsplus.services; -import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -9,7 +8,6 @@ import android.content.Intent; 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; @@ -27,12 +25,11 @@ 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.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.tzur.conversations.Settings; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; @@ -67,9 +64,7 @@ public class NotificationService { && !message.isRead() && ConversationsPlusPreferences.showNotification() && !message.getConversation().isMuted() - && (message.getConversation().getMode() == Conversation.MODE_SINGLE - || ConversationsPlusPreferences.alwaysNotifyInConference() - || wasHighlightedOrPrivate(message) + && (message.getConversation().alwaysNotify() || wasHighlightedOrPrivate(message) ); } @@ -109,47 +104,47 @@ public class NotificationService { } } - @SuppressLint("NewApi") - @SuppressWarnings("deprecation") - private boolean isInteractive() { - final PowerManager pm = (PowerManager) mXmppConnectionService - .getSystemService(Context.POWER_SERVICE); + public void pushFromBacklog(final Message message) { + if (notify(message)) { + synchronized (notifications) { + pushToStack(message); + } + } + } - final boolean isScreenOn; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - isScreenOn = pm.isScreenOn(); - } else { - isScreenOn = pm.isInteractive(); + public void finishBacklog(boolean notify) { + synchronized (notifications) { + mXmppConnectionService.updateUnreadCountBadge(); + updateNotification(notify); } + } - return isScreenOn; + private void pushToStack(final Message message) { + 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); + } } public void push(final Message message) { if (!notify(message)) { return; } - mXmppConnectionService.updateUnreadCountBadge(); - - final boolean isScreenOn = isInteractive(); - + mXmppConnectionService.updateUnreadCountBadge(); + final boolean isScreenOn = mXmppConnectionService.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); - } + pushToStack(message); final Account account = message.getConversation().getAccount(); final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) - && !account.inGracePeriod() - && !this.inMiniGracePeriod(account); + && !account.inGracePeriod() + && !this.inMiniGracePeriod(account); updateNotification(doNotify); if (doNotify) { notifyPebble(message); @@ -172,10 +167,10 @@ public class NotificationService { } private void setNotificationColor(final Builder mBuilder) { - mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.notification)); + mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary)); } - private void updateNotification(final boolean notify) { + public void updateNotification(final boolean notify) { final NotificationManager notificationManager = (NotificationManager) ConversationsPlusApplication.getAppContext().getSystemService(Context.NOTIFICATION_SERVICE); final String ringtone = ConversationsPlusPreferences.notificationRingtone(); @@ -230,8 +225,13 @@ public class NotificationService { 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)); + if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { + int count = messages.size(); + style.addLine(Html.fromHtml("<b>"+name+"</b>: "+mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages,count,count))); + } else { + style.addLine(Html.fromHtml("<b>" + name + "</b>: " + + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first)); + } names.append(name); names.append(", "); } @@ -259,25 +259,32 @@ public class NotificationService { final Conversation conversation = messages.get(0).getConversation(); mBuilder.setLargeIcon(AvatarService.getInstance().get(conversation, getPixel(64))); mBuilder.setContentTitle(conversation.getName()); - Message message; - if ((message = getImage(messages)) != null) { - modifyForImage(mBuilder, message, messages, notify); + if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { + int count = messages.size(); + mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages,count,count)); } 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)); + Message message; + if ((message = getImage(messages)) != null) { + modifyForImage(mBuilder, message, messages, notify); + } else if (conversation.getMode() == Conversation.MODE_MULTI) { + modifyForConference(mBuilder, conversation, 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)); } @@ -285,7 +292,7 @@ public class NotificationService { } private void modifyForImage(final Builder builder, final Message message, - final ArrayList<Message> messages, final boolean notify) { + final ArrayList<Message> messages, final boolean notify) { try { final Bitmap bitmap = ImageUtil.getThumbnail(message, getPixel(288), false); final ArrayList<Message> tmp = new ArrayList<>(); @@ -293,17 +300,17 @@ public class NotificationService { if (msg.getType() == Message.TYPE_TEXT && msg.getTransferable() == 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); + builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, tmp.get(0)).first); } else { builder.setContentText(mXmppConnectionService.getString( R.string.received_x_file, - UIHelper.getFileDescriptionString(mXmppConnectionService,message))); + UIHelper.getFileDescriptionString(mXmppConnectionService, message))); } builder.setStyle(bigPictureStyle); } catch (final FileNotFoundException e) { @@ -312,19 +319,40 @@ public class NotificationService { } private void modifyForTextOnly(final Builder builder, - final ArrayList<Message> messages, final boolean notify) { + final ArrayList<Message> messages, final boolean notify) { builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages))); - builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(0)).first); + builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first); if (notify) { - builder.setTicker(UIHelper.getMessagePreview(mXmppConnectionService,messages.get(messages.size() - 1)).first); + builder.setTicker(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first); + } + } + + private void modifyForConference(Builder builder, Conversation conversation, List<Message> messages, boolean notify) { + final Message first = messages.get(0); + final Message last = messages.get(messages.size() - 1); + final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); + style.setBigContentTitle(conversation.getName()); + + for(Message message : messages) { + if (message.hasMeCommand()) { + style.addLine(UIHelper.getMessagePreview(mXmppConnectionService,message).first); + } else { + style.addLine(Html.fromHtml("<b>" + UIHelper.getMessageDisplayName(message) + "</b>: " + UIHelper.getMessagePreview(mXmppConnectionService, message).first)); + } + } + builder.setContentText((first.hasMeCommand() ? "" :UIHelper.getMessageDisplayName(first)+ ": ") +UIHelper.getMessagePreview(mXmppConnectionService, first).first); + builder.setStyle(style); + if (notify) { + builder.setTicker((last.hasMeCommand() ? "" : UIHelper.getMessageDisplayName(last) + ": ") + UIHelper.getMessagePreview(mXmppConnectionService,last).first); } } private Message getImage(final Iterable<Message> messages) { for (final Message message : messages) { - if (message.getType() == Message.TYPE_IMAGE + if (message.getType() != Message.TYPE_TEXT && message.getTransferable() == null - && message.getEncryption() != Message.ENCRYPTION_PGP) { + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getFileParams().height > 0) { return message; } } @@ -342,7 +370,7 @@ public class NotificationService { } private Message getFirstLocationMessage(final Iterable<Message> messages) { - for(final Message message : messages) { + for (final Message message : messages) { if (GeoHelper.isGeoUri(message.getBody())) { return message; } @@ -353,7 +381,7 @@ public class NotificationService { 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); + text.append(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(i)).first); if (i != messages.size() - 1) { text.append("\n"); } @@ -363,9 +391,9 @@ public class NotificationService { private PendingIntent createShowLocationIntent(final Message message) { Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(message); - for(Intent intent : intents) { + for (Intent intent : intents) { if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) { - return PendingIntent.getActivity(mXmppConnectionService,18,intent,PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getActivity(mXmppConnectionService, 18, intent, PendingIntent.FLAG_UPDATE_CURRENT); } } return createOpenConversationsIntent(); @@ -373,7 +401,7 @@ public class NotificationService { private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) { final TaskStackBuilder stackBuilder = TaskStackBuilder - .create(mXmppConnectionService); + .create(mXmppConnectionService); stackBuilder.addParentStack(ConversationActivity.class); final Intent viewConversationIntent = new Intent(mXmppConnectionService, @@ -425,30 +453,14 @@ public class NotificationService { } private PendingIntent createDisableAccountIntent(final Account account) { - final Intent intent = new Intent(mXmppConnectionService,XmppConnectionService.class); + 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); - } - - public 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); + intent.putExtra("account", account.getJid().toBareJid().toString()); + return PendingIntent.getService(mXmppConnectionService, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } - 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" + Pattern.quote(nick) + "\\p{Punct}?\\b", - Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + private boolean wasHighlightedOrPrivate(final Message message) { + return MessageUtil.wasHighlightedOrPrivate(message); } public void setOpenConversation(final Conversation conversation) { @@ -461,7 +473,7 @@ public class NotificationService { private int getPixel(final int dp) { final DisplayMetrics metrics = mXmppConnectionService.getResources() - .getDisplayMetrics(); + .getDisplayMetrics(); return ((int) (dp * metrics.density)); } @@ -471,7 +483,7 @@ public class NotificationService { private boolean inMiniGracePeriod(final Account account) { final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD - : Config.MINI_GRACE_PERIOD * 2; + : Config.MINI_GRACE_PERIOD * 2; return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace); } @@ -479,41 +491,57 @@ public class NotificationService { 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)); + if (Config.SHOW_CONNECTED_ACCOUNTS) { + List<Account> accounts = mXmppConnectionService.getAccounts(); + int enabled = 0; + int connected = 0; + for (Account account : accounts) { + if (account.isOnlineAndConnected()) { + connected++; + enabled++; + } else if (!account.isOptionSet(Account.OPTION_DISABLED)) { + enabled++; + } + } + mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled)); + } else { + mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_open_conversations)); + } mBuilder.setContentIntent(createOpenConversationsIntent()); mBuilder.setWhen(0); - mBuilder.setPriority(NotificationCompat.PRIORITY_MIN); + mBuilder.setPriority(Config.SHOW_CONNECTED_ACCOUNTS ? NotificationCompat.PRIORITY_DEFAULT : 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_24dp); cancelIcon = R.drawable.ic_cancel_white_24dp; } else { - mBuilder.setSmallIcon(R.drawable.ic_stat_communication_import_export); cancelIcon = R.drawable.ic_action_cancel; } + mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp); 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); + return PendingIntent.getActivity(mXmppConnectionService, 0, new Intent(mXmppConnectionService, ConversationActivity.class), 0); } public void updateErrorNotification() { - final NotificationManager mNotificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); + final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); final List<Account> errors = new ArrayList<>(); for (final Account account : mXmppConnectionService.getAccounts()) { if (account.hasErrorStatus()) { errors.add(account); } } + if (mXmppConnectionService.getPreferences().getBoolean("keep_foreground_service", false)) { + notificationManager.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); + } final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); if (errors.size() == 0) { - mNotificationManager.cancel(ERROR_NOTIFICATION_ID); + notificationManager.cancel(ERROR_NOTIFICATION_ID); return; } else if (errors.size() == 1) { mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account)); @@ -540,12 +568,12 @@ public class NotificationService { final TaskStackBuilder stackBuilder = TaskStackBuilder.create(mXmppConnectionService); stackBuilder.addParentStack(ConversationActivity.class); - final Intent manageAccountsIntent = new Intent(mXmppConnectionService,ManageAccountActivity.class); + final Intent manageAccountsIntent = new Intent(mXmppConnectionService, ManageAccountActivity.class); stackBuilder.addNextIntent(manageAccountsIntent); - final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT); + final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(resultPendingIntent); - mNotificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build()); + notificationManager.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 index bc982466..df5f72f5 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java @@ -6,11 +6,16 @@ import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.media.AudioManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Binder; +import android.os.Build; import android.os.Bundle; import android.os.FileObserver; import android.os.IBinder; @@ -18,7 +23,13 @@ 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.security.KeyChain; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.LruCache; +import android.util.Pair; import net.java.otr4j.OtrException; import net.java.otr4j.session.Session; @@ -26,24 +37,28 @@ import net.java.otr4j.session.SessionID; import net.java.otr4j.session.SessionImpl; import net.java.otr4j.session.SessionStatus; +import org.openintents.openpgp.IOpenPgpService2; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; import java.math.BigInteger; import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Hashtable; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import de.duenndns.ssl.MemorizingTrustManager; - import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; @@ -57,16 +72,21 @@ 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.crypto.axolotl.AxolotlService; +import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlMessage; 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.Transferable; -import de.thedevstack.conversationsplus.entities.TransferablePlaceholder; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.MucOptions; import de.thedevstack.conversationsplus.entities.MucOptions.OnRenameListener; +import de.thedevstack.conversationsplus.entities.Presence; +import de.thedevstack.conversationsplus.entities.Roster; +import de.thedevstack.conversationsplus.entities.ServiceDiscoveryResult; +import de.thedevstack.conversationsplus.entities.Transferable; +import de.thedevstack.conversationsplus.entities.TransferablePlaceholder; import de.thedevstack.conversationsplus.generator.IqGenerator; import de.thedevstack.conversationsplus.generator.MessageGenerator; import de.thedevstack.conversationsplus.generator.PresenceGenerator; @@ -87,6 +107,7 @@ 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.OnKeyStatusUpdated; import de.thedevstack.conversationsplus.xmpp.OnMessageAcknowledged; import de.thedevstack.conversationsplus.xmpp.OnMessagePacketReceived; import de.thedevstack.conversationsplus.xmpp.OnPresencePacketReceived; @@ -110,9 +131,16 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa 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 static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static final String ACTION_GCM_TOKEN_REFRESH = "gcm_token_refresh"; + public static final String ACTION_GCM_MESSAGE_RECEIVED = "gcm_message_received"; + private final IBinder mBinder = new XmppConnectionBinder(); + private final List<Conversation> conversations = new CopyOnWriteArrayList<>(); + private final IqGenerator mIqGenerator = new IqGenerator(); + private final List<String> mInProgressAvatarFetches = new ArrayList<>(); + public DatabaseBackend databaseBackend; private ContentObserver contactObserver = new ContentObserver(null) { @Override public void onChange(boolean selfChange) { @@ -123,59 +151,29 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa 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() { - + 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 OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() { @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); - } - } + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() != IqPacket.TYPE.RESULT) { + Element error = packet.findChild("error"); + String text = error != null ? error.findChildContent("text") : null; + if (text != null) { + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": received iq error - " + text); } } } }; - private final IqGenerator mIqGenerator = new IqGenerator(); - public DatabaseBackend databaseBackend; + private MessageGenerator mMessageGenerator = new MessageGenerator(); + private PresenceGenerator mPresenceGenerator = new PresenceGenerator(); + private List<Account> accounts; + private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( + this); public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { @Override @@ -202,84 +200,128 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } }; - private MemorizingTrustManager mMemorizingTrustManager; - private NotificationService mNotificationService = new NotificationService( + private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( this); - private OnMessagePacketReceived mMessageParser = new MessageParser(this); - private OnPresencePacketReceived mPresenceParser = new PresenceParser(this); - private IqParser mIqParser = new IqParser(this); - private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() { + private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); + private PushManagementService mPushManagementService = new PushManagementService(this); + private OnConversationUpdate mOnConversationUpdate = null; + private final FileObserver fileObserver = new FileObserver( + FileBackend.getConversationsImageDirectory()) { + @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.ERROR) { - Element error = packet.findChild("error"); - String text = error != null ? error.findChildContent("text") : null; - if (text != null) { - Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": received iq error - "+text); + 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 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); + } } } } }; - private MessageGenerator mMessageGenerator = new MessageGenerator(); - private PresenceGenerator mPresenceGenerator = new PresenceGenerator(); - private List<Account> accounts; - private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( - this); - private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( - this); - private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); - private OnConversationUpdate mOnConversationUpdate = null; private int convChangedListenerCount = 0; private OnShowErrorToast mOnShowErrorToast = null; private int showErrorToastListenerCount = 0; private int unreadCount = -1; private OnAccountUpdate mOnAccountUpdate = null; + private OnCaptchaRequested mOnCaptchaRequested = null; + private int accountChangedListenerCount = 0; + private int captchaRequestedListenerCount = 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 OnKeyStatusUpdated mOnKeyStatusUpdated = null; + private int keyStatusUpdatedListenerCount = 0; + private SecureRandom mRandom; + private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20); + private final OnBindListener mOnBindListener = new OnBindListener() { + + @Override + public void onBind(final Account account) { + account.getRoster().clearPresences(); + mJingleConnectionManager.cancelInTransmission(); + fetchRosterFromServer(account); + fetchBookmarks(account); + sendPresence(account); + if (mPushManagementService.available(account)) { + mPushManagementService.registerPushTokenOnServer(account); + } + connectMultiModeConversations(account); + syncDirtyContacts(account); + } + }; private OnStatusChanged statusListener = new OnStatusChanged() { @Override - public void onStatusChanged(Account account) { + public void onStatusChanged(final 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()) { - Logging.d(Config.LOGTAG, account.getJid().toBareJid() - + " sending csi//inactive"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//inactive"); connection.sendInactive(); } else { - Logging.d(Config.LOGTAG, account.getJid().toBareJid() - + " sending csi//active"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//active"); connection.sendActive(); } } - syncDirtyContacts(account); - scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode()); + List<Conversation> conversations = getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getAccount() == account + && !account.pendingConferenceJoins.contains(conversation)) { + if (!conversation.startOtrIfNeeded()) { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": couldn't start OTR with "+conversation.getContact().getJid()+" when needed"); + } + sendUnsentMessages(conversation); + } + } + for (Conversation conversation : account.pendingConferenceLeaves) { + leaveMuc(conversation); + } + account.pendingConferenceLeaves.clear(); + for (Conversation conversation : account.pendingConferenceJoins) { + joinMuc(conversation); + } + account.pendingConferenceJoins.clear(); + 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()); + final boolean disabled = account.isOptionSet(Account.OPTION_DISABLED); + final boolean pushMode = Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND + && mPushManagementService.available(account) + && checkListeners(); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode "+Boolean.toString(pushMode)); + if (!disabled && !pushMode) { + int timeToReconnect = mRandom.nextInt(20) + 10; + scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); } } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { databaseBackend.updateAccount(account); - reconnectAccount(account, true); + reconnectAccount(account, true, false); } else if ((account.getStatus() != Account.State.CONNECTING) && (account.getStatus() != Account.State.NO_INTERNET)) { if (connection != null) { @@ -288,53 +330,49 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa + ": error connecting account. try again in " + next + "s for the " + (connection.getAttempt() + 1) + " time"); - scheduleWakeUpCall(next,account.getUuid().hashCode()); + 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 EventReceiver mEventReceiver = new EventReceiver(); private boolean mRestoredFromDatabase = false; + public boolean areMessagesInitialized() { return this.mRestoredFromDatabase; } public PgpEngine getPgpEngine() { - if (pgpServiceConnection.isBound()) { + if (!Config.supportOpenPgp()) { + return null; + } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) { if (this.mPgpEngine == null) { this.mPgpEngine = new PgpEngine(new OpenPgpApi( - getApplicationContext(), - pgpServiceConnection.getService()), this); + getApplicationContext(), + pgpServiceConnection.getService()), this); } return mPgpEngine; } else { return null; } - } public void attachLocationToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { - int encryption = conversation.getNextEncryption(ConversationsPlusPreferences.forceEncryption()); + int encryption = conversation.getNextEncryption(); if (encryption == Message.ENCRYPTION_PGP) { encryption = Message.ENCRYPTION_DECRYPTED; } - Message message = new Message(conversation,uri.toString(),encryption); + Message message = new Message(conversation, uri.toString(), encryption); if (conversation.getNextCounterpart() != null) { message.setCounterpart(conversation.getNextCounterpart()); } @@ -346,16 +384,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void attachFileToConversation(final Conversation conversation, - final Uri uri, - final UiCallback<Message> callback) { + final Uri uri, + final UiCallback<Message> callback) { final Message message; - boolean forceEncryption = ConversationsPlusPreferences.forceEncryption(); - if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", - Message.ENCRYPTION_DECRYPTED); + if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); } else { - message = new Message(conversation, "", - conversation.getNextEncryption(forceEncryption)); + message = new Message(conversation, "", conversation.getNextEncryption()); } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_FILE); @@ -388,7 +423,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } - public Conversation find(Bookmark bookmark) { + public Conversation find(Bookmark bookmark) { return find(bookmark.getAccount(), bookmark.getJid()); } @@ -399,6 +434,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public int onStartCommand(Intent intent, int flags, int startId) { final String action = intent == null ? null : intent.getAction(); + boolean interactive = false; if (action != null) { switch (action) { case ConnectivityManager.CONNECTIVITY_ACTION: @@ -408,9 +444,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa break; case ACTION_MERGE_PHONE_CONTACTS: if (mRestoredFromDatabase) { - PhoneHelper.loadPhoneContacts(getApplicationContext(), - new CopyOnWriteArrayList<Bundle>(), - this); + loadPhoneContacts(); } return START_STICKY; case Intent.ACTION_SHUTDOWN: @@ -425,19 +459,36 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa break; case ACTION_TRY_AGAIN: resetAllAttemptCounts(false); + interactive = true; 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); + account.setOption(Account.OPTION_DISABLED, true); updateAccount(account); } } catch (final InvalidJidException ignored) { break; } break; + case AudioManager.RINGER_MODE_CHANGED_ACTION: + if (xaOnSilentMode()) { + refreshAllPresences(); + } + break; + case Intent.ACTION_SCREEN_OFF: + case Intent.ACTION_SCREEN_ON: + if (awayWhenScreenOff()) { + refreshAllPresences(); + } + break; + case ACTION_GCM_TOKEN_REFRESH: + refreshAllGcmTokens(); + break; + case ACTION_GCM_MESSAGE_RECEIVED: + Log.d(Config.LOGTAG,"gcm push message arrived in service. extras="+intent.getExtras()); } } this.wakeLock.acquire(); @@ -460,36 +511,42 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa 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(); + long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime(); long pingTimeoutIn = (lastSent + Config.PING_TIMEOUT * 1000) - SystemClock.elapsedRealtime(); if (lastSent > lastReceived) { if (pingTimeoutIn < 0) { Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout"); - this.reconnectAccount(account, true); + this.reconnectAccount(account, true, interactive); } else { int secs = (int) (pingTimeoutIn / 1000); - this.scheduleWakeUpCall(secs,account.getUuid().hashCode()); + this.scheduleWakeUpCall(secs, account.getUuid().hashCode()); } } else if (msToNextPing <= 0) { account.getXmppConnection().sendPing(); - Logging.d(Config.LOGTAG, account.getJid().toBareJid()+" send ping"); - this.scheduleWakeUpCall(Config.PING_TIMEOUT,account.getUuid().hashCode()); + Logging.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); + reconnectAccount(account, true, interactive); } else if (account.getStatus() == Account.State.CONNECTING) { - long timeout = Config.CONNECT_TIMEOUT - ((SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000); + long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000; + long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000; + long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco; + long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; if (timeout < 0) { Logging.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting"); - reconnectAccount(account, true); + reconnectAccount(account, true, interactive); + } else if (discoTimeout < 0) { + account.getXmppConnection().sendDiscoTimeout(); + scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode()); } else { - scheduleWakeUpCall((int) timeout,account.getUuid().hashCode()); + scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode()); } } else { if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { - reconnectAccount(account, true); + reconnectAccount(account, true, interactive); } } @@ -499,10 +556,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } } - /*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE); - if (!pm.isScreenOn()) { - removeStaleListeners(); - }*/ if (wakeLock.isHeld()) { try { wakeLock.release(); @@ -512,9 +565,46 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return START_STICKY; } + private boolean xaOnSilentMode() { + return getPreferences().getBoolean("xa_on_silent_mode", false); + } + + private boolean awayWhenScreenOff() { + return getPreferences().getBoolean("away_when_screen_off", false); + } + + private Presence.Status getTargetPresence() { + if (xaOnSilentMode() && isPhoneSilenced()) { + return Presence.Status.XA; + } else if (awayWhenScreenOff() && !isInteractive()) { + return Presence.Status.AWAY; + } else { + return Presence.Status.ONLINE; + } + } + + @SuppressLint("NewApi") + @SuppressWarnings("deprecation") + public boolean isInteractive() { + final PowerManager pm = (PowerManager) 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; + } + + private boolean isPhoneSilenced() { + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + return audioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT; + } + private void resetAllAttemptCounts(boolean reallyAll) { Logging.d(Config.LOGTAG, "resetting all attepmt counts"); - for(Account account : accounts) { + for (Account account : accounts) { if (account.hasErrorStatus() || reallyAll) { final XmppConnection connection = account.getXmppConnection(); if (connection != null) { @@ -526,7 +616,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public boolean hasInternetConnection() { ConnectivityManager cm = (ConnectivityManager) getApplicationContext() - .getSystemService(Context.CONNECTIVITY_SERVICE); + .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); return activeNetwork != null && activeNetwork.isConnected(); } @@ -557,25 +647,80 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa PRNGFixes.apply(); this.mRandom = new SecureRandom(); updateMemorizingTrustmanager(); + 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.initAccountServices(this); - } restoreFromDatabase(); getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver); this.fileObserver.startWatching(); - this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain"); - this.pgpServiceConnection.bindToService(); + + if (Config.supportOpenPgp()) { + this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() { + @Override + public void onBound(IOpenPgpService2 service) { + for (Account account : accounts) { + if (account.getPgpDecryptionService() != null) { + account.getPgpDecryptionService().onOpenPgpServiceBound(); + } + } + } + + @Override + public void onError(Exception e) { + } + }); + this.pgpServiceConnection.bindToService(); + } this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"XmppConnectionService"); + this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService"); toggleForegroundService(); updateUnreadCountBadge(); UiUpdateHelper.initXmppConnectionService(this); + toggleScreenEventReceiver(); + } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + if (level >= TRIM_MEMORY_COMPLETE) { + Log.d(Config.LOGTAG, "clear cache due to low memory"); + getBitmapCache().evictAll(); + } + } + + @Override + public void onDestroy() { + try { + unregisterReceiver(this.mEventReceiver); + } catch (IllegalArgumentException e) { + //ignored + } + super.onDestroy(); + } + + public void toggleScreenEventReceiver() { + if (awayWhenScreenOff()) { + final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + registerReceiver(this.mEventReceiver, filter); + } else { + try { + unregisterReceiver(this.mEventReceiver); + } catch (IllegalArgumentException e) { + //ignored + } + } } public void toggleForegroundService() { @@ -598,19 +743,27 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for (final Account account : accounts) { databaseBackend.writeRoster(account.getRoster()); if (account.getXmppConnection() != null) { - disconnect(account, false); + new Thread(new Runnable() { + @Override + public void run() { + disconnect(account, false); + } + }).start(); + cancelWakeUpCall(account.getUuid().hashCode()); } } - 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)); Logging.d(Config.LOGTAG, "good bye"); stopSelf(); } - protected void scheduleWakeUpCall(int seconds, int requestCode) { + private void cancelWakeUpCall(int requestCode) { + final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + final Intent intent = new Intent(this, EventReceiver.class); + intent.setAction("ping"); + alarmManager.cancel(PendingIntent.getBroadcast(this, requestCode, intent, 0)); + } + + public void scheduleWakeUpCall(int seconds, int requestCode) { final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000; Context context = getApplicationContext(); @@ -633,6 +786,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa connection.setOnBindListener(this.mOnBindListener); connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); + AxolotlService axolotlService = account.getAxolotlService(); + if (axolotlService != null) { + connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); + } return connection; } @@ -643,37 +800,41 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } - private void sendFileMessage(final Message message) { + private void sendFileMessage(final Message message, final boolean delay) { Logging.d(Config.LOGTAG, "send file message"); final Account account = message.getConversation().getAccount(); final XmppConnection connection = account.getXmppConnection(); if (connection != null && connection.getFeatures().httpUpload()) { - mHttpConnectionManager.createNewUploadConnection(message); + mHttpConnectionManager.createNewUploadConnection(message, delay); } else { mJingleConnectionManager.createNewConnection(message); } } public void sendMessage(final Message message) { - sendMessage(message, false); + sendMessage(message, false, false); } - private void sendMessage(final Message message, final boolean resend) { + private void sendMessage(final Message message, final boolean resend, final boolean delay) { final Account account = message.getConversation().getAccount(); final Conversation conversation = message.getConversation(); account.deactivateGracePeriod(); MessagePacket packet = null; - boolean saveInDb = true; + final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI + || account.getServerIdentity() != XmppConnection.Identity.SLACK) + && !message.edited(); + boolean saveInDb = addToConversation; message.setStatus(Message.STATUS_WAITING); if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) { message.getConversation().endOtrIfNeeded(); - message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { - @Override - public void onMessageFound(Message message) { - markMessage(message,Message.STATUS_SEND_FAILED); - } - }); + message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, + new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + markMessage(message, Message.STATUS_SEND_FAILED); + } + }); } if (account.isOnlineAndConnected()) { @@ -681,24 +842,24 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa case Message.ENCRYPTION_NONE: if (message.needsUploading()) { if (account.httpUploadAvailable() || message.fixCounterpart()) { - this.sendFileMessage(message); + this.sendFileMessage(message, delay); } else { break; } } else { - packet = mMessageGenerator.generateChat(message,resend); + packet = mMessageGenerator.generateChat(message); } break; case Message.ENCRYPTION_PGP: case Message.ENCRYPTION_DECRYPTED: if (message.needsUploading()) { if (account.httpUploadAvailable() || message.fixCounterpart()) { - this.sendFileMessage(message); + this.sendFileMessage(message, delay); } else { break; } } else { - packet = mMessageGenerator.generatePgpChat(message,resend); + packet = mMessageGenerator.generatePgpChat(message); } break; case Message.ENCRYPTION_OTR: @@ -712,16 +873,37 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (message.needsUploading()) { mJingleConnectionManager.createNewConnection(message); } else { - packet = mMessageGenerator.generateOtrChat(message,resend); + packet = mMessageGenerator.generateOtrChat(message); } } else if (otrSession == null) { if (message.fixCounterpart()) { conversation.startOtrSession(message.getCounterpart().getResourcepart(), true); } else { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fix counterpart for OTR message to contact "+message.getContact().getJid()); break; } + } else { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+" OTR session with "+message.getContact()+" is in wrong state: "+otrSession.getSessionStatus().toString()); + } + break; + case Message.ENCRYPTION_AXOLOTL: + message.setAxolotlFingerprint(account.getAxolotlService().getOwnFingerprint()); + if (message.needsUploading()) { + if (account.httpUploadAvailable() || message.fixCounterpart()) { + this.sendFileMessage(message, delay); + } else { + break; + } + } else { + XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message); + if (axolotlMessage == null) { + account.getAxolotlService().preparePayloadMessage(message, delay); + } else { + packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage); + } } break; + } if (packet != null) { if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) { @@ -731,7 +913,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } } else { - switch(message.getEncryption()) { + switch (message.getEncryption()) { case Message.ENCRYPTION_DECRYPTED: if (!message.needsUploading()) { String pgpBody = message.getEncryptedBody(); @@ -746,28 +928,41 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa break; case Message.ENCRYPTION_OTR: if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": create otr session without starting for "+message.getContact().getJid()); conversation.startOtrSession(message.getCounterpart().getResourcepart(), false); } break; + case Message.ENCRYPTION_AXOLOTL: + message.setAxolotlFingerprint(account.getAxolotlService().getOwnFingerprint()); + break; } } if (resend) { - if (packet != null) { + if (packet != null && addToConversation) { if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) { - markMessage(message,Message.STATUS_UNSEND); + markMessage(message, Message.STATUS_UNSEND); } else { - markMessage(message,Message.STATUS_SEND); + markMessage(message, Message.STATUS_SEND); } } } else { - conversation.add(message); - if (saveInDb && (message.getEncryption() == Message.ENCRYPTION_NONE || !ConversationsPlusPreferences.dontSaveEncrypted())) { - databaseBackend.createMessage(message); + if (addToConversation) { + conversation.add(message); + } + if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) { + if (saveInDb) { + databaseBackend.createMessage(message); + } else if (message.edited()) { + databaseBackend.updateMessage(message, message.getEditedId()); + } } updateConversationUi(); } if (packet != null) { + if (delay) { + mMessageGenerator.addDelay(packet, message.getTimeSent()); + } if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { if (ConversationsPlusPreferences.chatStates()) { packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); @@ -782,13 +977,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void onMessageFound(Message message) { - resendMessage(message); + resendMessage(message, true); } }); } - public void resendMessage(final Message message) { - sendMessage(message, true); + public void resendMessage(final Message message, final boolean delay) { + sendMessage(message, true, delay); } public void fetchRosterFromServer(final Account account) { @@ -811,34 +1006,42 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @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); + if (packet.getType() == IqPacket.TYPE.RESULT) { + final Element query = packet.query(); + final HashMap<Jid, Bookmark> bookmarks = new HashMap<>(); + final Element storage = query.findChild("storage", "storage:bookmarks"); + final boolean autojoin = respectAutojoin(); + if (storage != null) { + for (final Element item : storage.getChildren()) { + if (item.getName().equals("conference")) { + final Bookmark bookmark = Bookmark.parse(item, account); + Bookmark old = bookmarks.put(bookmark.getJid(), bookmark); + if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) { + bookmark.setBookmarkName(old.getBookmarkName()); + } + Conversation conversation = find(bookmark); + if (conversation != null) { + conversation.setBookmark(bookmark); + } else if (bookmark.autojoin() && bookmark.getJid() != null && autojoin) { + conversation = findOrCreateConversation( + account, bookmark.getJid(), true); + conversation.setBookmark(bookmark); + joinMuc(conversation); + } } } } + account.setBookmarks(new ArrayList<>(bookmarks.values())); + } else { + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not fetch bookmarks"); } - account.setBookmarks(bookmarks); } }; sendIqPacket(account, iqPacket, callback); } public void pushBookmarks(Account account) { + Logging.d(Config.LOGTAG, account.getJid().toBareJid()+": pushing bookmarks"); IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); Element query = iqPacket.query("jabber:iq:private"); Element storage = query.addChild("storage", "storage:bookmarks"); @@ -855,7 +1058,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa mPhoneContactMergerThread = new Thread(new Runnable() { @Override public void run() { - Logging.d(Config.LOGTAG,"start merging phone contacts with roster"); + Logging.d(Config.LOGTAG, "start merging phone contacts with roster"); for (Account account : accounts) { List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(); for (Bundle phoneContact : phoneContacts) { @@ -871,8 +1074,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } final Contact contact = account.getRoster().getContact(jid); String systemAccount = phoneContact.getInt("phoneid") - + "#" - + phoneContact.getString("lookup"); + + "#" + + phoneContact.getString("lookup"); contact.setSystemAccount(systemAccount); if (contact.setPhotoUri(phoneContact.getString("photouri"))) { AvatarService.getInstance().clear(contact); @@ -880,7 +1083,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa contact.setSystemName(phoneContact.getString("displayname")); withSystemAccounts.remove(contact); } - for(Contact contact : withSystemAccounts) { + for (Contact contact : withSystemAccounts) { contact.setSystemAccount(null); contact.setSystemName(null); if (contact.setPhotoUri(null)) { @@ -906,23 +1109,29 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Account account = accountLookupTable.get(conversation.getAccountUuid()); conversation.setAccount(account); } - Runnable runnable =new Runnable() { + Runnable runnable = new Runnable() { @Override public void run() { - Logging.d(Config.LOGTAG,"restoring roster"); - for(Account account : accounts) { + Logging.d(Config.LOGTAG, "restoring roster"); + for (Account account : accounts) { databaseBackend.readRoster(account.getRoster()); + account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage } ImageUtil.evictBitmapCache(); Looper.prepare(); - PhoneHelper.loadPhoneContacts(getApplicationContext(), - new CopyOnWriteArrayList<Bundle>(), - XmppConnectionService.this); - Logging.d(Config.LOGTAG,"restoring messages"); + loadPhoneContacts(); + Logging.d(Config.LOGTAG, "restoring messages"); for (Conversation conversation : conversations) { conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); checkDeletedFiles(conversation); + conversation.findUnreadMessages(new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + mNotificationService.pushFromBacklog(message); + } + }); } + mNotificationService.finishBacklog(false); mRestoredFromDatabase = true; Logging.d(Config.LOGTAG,"restored all messages"); updateConversationUi(); @@ -932,6 +1141,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void loadPhoneContacts() { + PhoneHelper.loadPhoneContacts(getApplicationContext(), + new CopyOnWriteArrayList<Bundle>(), + XmppConnectionService.this); + } + public List<Conversation> getConversations() { return this.conversations; } @@ -943,6 +1158,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void onMessageFound(Message message) { if (!FileBackend.isFileAvailable(message)) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); + final int s = message.getStatus(); + if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) { + markMessage(message, Message.STATUS_SEND_FAILED); + } } } }); @@ -954,7 +1173,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (message != null) { if (!FileBackend.isFileAvailable(message)) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); - updateConversationUi(); + final int s = message.getStatus(); + if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) { + markMessage(message, Message.STATUS_SEND_FAILED); + } else { + updateConversationUi(); + } } return; } @@ -994,12 +1218,15 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { - Logging.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); - if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) { + if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) { Logging.d("mam", "Query in progress"); return; + } else if (timestamp == 0) { + Logging.d("mam", "Query stopped due to timestamp"); + return; } //TODO Create a separate class for this runnable to store if messages are getting loaded or not. Not really a good idea to do this in the callback. + Logging.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); Runnable runnable = new Runnable() { @Override public void run() { @@ -1007,30 +1234,32 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (null != callback) { callback.setLoadingInProgress(); // Tell the callback that the loading is in progress } - final Account account = conversation.getAccount(); - List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp); + final Account account = conversation.getAccount(); + List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp); Logging.d("mam", "runnable load more messages"); - if (messages.size() > 0) { + if (messages.size() > 0) { Logging.d("mam", "At least one message"); - conversation.addAll(0, messages); - checkDeletedFiles(conversation); - callback.onMoreMessagesLoaded(messages.size(), conversation); - } else if (conversation.hasMessagesLeftOnServer() - && account.isOnlineAndConnected() - && account.getXmppConnection().getFeatures().mam()) { + conversation.addAll(0, messages); + checkDeletedFiles(conversation); + callback.onMoreMessagesLoaded(messages.size(), conversation); + } else if (conversation.hasMessagesLeftOnServer() + && account.isOnlineAndConnected()) { Logging.d("mam", "mam activate, account online and connected and messages left on server"); - MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp - 1); - if (query != null) { - query.setCallback(callback); - } - callback.informUser(R.string.fetching_history_from_server); - } else { + if ((conversation.getMode() == Conversation.MODE_SINGLE && account.getXmppConnection().getFeatures().mam()) + || (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().mamSupport())) { + MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp - 1); + if (query != null) { + query.setCallback(callback); + } + callback.informUser(R.string.fetching_history_from_server); + } + } else { Logging.d("mam", ((!conversation.hasMessagesLeftOnServer()) ? "no" : "") + " more messages left on server, mam " + ((account.getXmppConnection().getFeatures().mam()) ? "" : "not") + " activated, account is " + ((account.isOnlineAndConnected()) ? "" : "not") + " online or connected)"); callback.onMoreMessagesLoaded(0, conversation); callback.informUser(R.string.no_more_history_on_server); } + } } - } }; ConversationsPlusApplication.executeDatabaseOperation(runnable); } @@ -1128,7 +1357,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getAccount().getStatus() == Account.State.ONLINE) { Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null && bookmark.autojoin()) { + if (bookmark != null && bookmark.autojoin() && respectAutojoin()) { bookmark.setAutojoin(false); pushBookmarks(bookmark.getAccount()); } @@ -1136,6 +1365,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa leaveMuc(conversation); } else { conversation.endOtrIfNeeded(); + if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + Log.d(Config.LOGTAG, "Canceling presence request from " + conversation.getJid().toString()); + sendPresencePacket( + conversation.getAccount(), + mPresenceGenerator.stopPresenceUpdatesTo(conversation.getContact()) + ); + } } this.databaseBackend.updateConversation(conversation); this.conversations.remove(conversation); @@ -1151,10 +1387,68 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa updateAccountUi(); } + public void createAccountFromKey(final String alias, final OnAccountCreated callback) { + new Thread(new Runnable() { + @Override + public void run() { + try { + X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias); + Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]); + if (findAccountByJid(info.first) == null) { + Account account = new Account(info.first, ""); + account.setPrivateKeyAlias(alias); + account.setOption(Account.OPTION_DISABLED, true); + account.setDisplayName(info.second); + createAccount(account); + callback.onAccountCreated(account); + if (Config.X509_VERIFICATION) { + try { + getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA"); + } catch (CertificateException e) { + callback.informUser(R.string.certificate_chain_is_not_trusted); + } + } + } else { + callback.informUser(R.string.account_already_exists); + } + } catch (Exception e) { + e.printStackTrace(); + callback.informUser(R.string.unable_to_parse_certificate); + } + } + }).start(); + + } + + public void updateKeyInAccount(final Account account, final String alias) { + Log.d(Config.LOGTAG, "update key in account " + alias); + try { + X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias); + Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]); + if (account.getJid().toBareJid().equals(info.first)) { + account.setPrivateKeyAlias(alias); + account.setDisplayName(info.second); + databaseBackend.updateAccount(account); + if (Config.X509_VERIFICATION) { + try { + getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA"); + } catch (CertificateException e) { + showErrorToastInUi(R.string.certificate_chain_is_not_trusted); + } + account.getAxolotlService().regenerateKeys(true); + } + } else { + showErrorToastInUi(R.string.jid_does_not_match_certificate); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + public void updateAccount(final Account account) { this.statusListener.onStatusChanged(account); databaseBackend.updateAccount(account); - reconnectAccount(account, false); + reconnectAccountInBackground(account); updateAccountUi(); getNotificationService().updateErrorNotification(); } @@ -1190,7 +1484,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (account.getXmppConnection() != null) { this.disconnect(account, true); } - databaseBackend.deleteAccount(account); + Runnable runnable = new Runnable() { + @Override + public void run() { + databaseBackend.deleteAccount(account); + } + }; + ConversationsPlusApplication.executeDatabaseOperation(runnable); this.accounts.remove(account); updateAccountUi(); getNotificationService().updateErrorNotification(); @@ -1275,6 +1575,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnCaptchaRequested = listener; + if (this.captchaRequestedListenerCount < 2) { + this.captchaRequestedListenerCount++; + } + } + } + + public void removeOnCaptchaRequestedListener() { + synchronized (this) { + this.captchaRequestedListenerCount--; + if (this.captchaRequestedListenerCount <= 0) { + this.mOnCaptchaRequested = null; + this.captchaRequestedListenerCount = 0; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + public void setOnRosterUpdateListener(final OnRosterUpdate listener) { synchronized (this) { if (checkListeners()) { @@ -1325,6 +1650,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnKeyStatusUpdated = listener; + if (this.keyStatusUpdatedListenerCount < 2) { + this.keyStatusUpdatedListenerCount++; + } + } + } + + public void removeOnNewKeysAvailableListener() { + synchronized (this) { + this.keyStatusUpdatedListenerCount--; + if (this.keyStatusUpdatedListenerCount <= 0) { + this.keyStatusUpdatedListenerCount = 0; + this.mOnKeyStatusUpdated = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { synchronized (this) { if (checkListeners()) { @@ -1354,11 +1704,16 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return (this.mOnAccountUpdate == null && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null + && this.mOnCaptchaRequested == null && this.mOnUpdateBlocklist == null - && this.mOnShowErrorToast == null); + && this.mOnShowErrorToast == null + && this.mOnKeyStatusUpdated == null); } private void switchToForeground() { + for (Conversation conversation : getConversations()) { + conversation.setIncomingChatState(ChatState.ACTIVE); + } for (Account account : getAccounts()) { if (account.getStatus() == Account.State.ONLINE) { XmppConnection connection = account.getXmppConnection(); @@ -1374,14 +1729,17 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for (Account account : getAccounts()) { if (account.getStatus() == Account.State.ONLINE) { XmppConnection connection = account.getXmppConnection(); - if (connection != null && connection.getFeatures().csi()) { - connection.sendInactive(); + if (connection != null) { + if (connection.getFeatures().csi()) { + connection.sendInactive(); + } + if (Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND && mPushManagementService.available(account)) { + connection.waitForPush(); + cancelWakeUpCall(account.getUuid().hashCode()); + } } } } - for(Conversation conversation : getConversations()) { - conversation.setIncomingChatState(ChatState.ACTIVE); - } this.mNotificationService.setIsInForeground(false); Logging.d(Config.LOGTAG, "app switched into background"); } @@ -1389,47 +1747,80 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private void connectMultiModeConversations(Account account) { List<Conversation> conversations = getConversations(); for (Conversation conversation : conversations) { - if ((conversation.getMode() == Conversation.MODE_MULTI) - && (conversation.getAccount() == account)) { - conversation.resetMucOptions(); + if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) { joinMuc(conversation); } } } public void joinMuc(Conversation conversation) { + joinMuc(conversation, null); + } + + private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) { 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 - } - Logging.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.resetMucOptions(); conversation.setHasMessagesLeftOnServer(false); + fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() { + + private void join(Conversation conversation) { + Account account = conversation.getAccount(); + final String nick = conversation.getMucOptions().getProposedNick(); + final Jid joinJid = conversation.getMucOptions().createJoinJid(nick); + 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()); + } + + if (conversation.getMucOptions().mamSupport()) { + // Use MAM instead of the limited muc history to get history + x.addChild("history").setAttribute("maxchars", "0"); + } else { + // Fallback to muc history + x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted())); + } + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + sendPresencePacket(account, packet); + if (onConferenceJoined != null) { + onConferenceJoined.onConferenceJoined(conversation); + } + if (!joinJid.equals(conversation.getJid())) { + conversation.setContactJid(joinJid); + databaseBackend.updateConversation(conversation); + } + if (conversation.getMucOptions().mamSupport()) { + getMessageArchiveService().catchupMUC(conversation); + } + sendUnsentMessages(conversation); + } + + @Override + public void onConferenceConfigurationFetched(Conversation conversation) { + join(conversation); + } + + @Override + public void onFetchFailed(final Conversation conversation, Element error) { + join(conversation); + fetchConferenceConfiguration(conversation); + } + }); + updateConversationUi(); } else { account.pendingConferenceJoins.add(conversation); + conversation.resetMucOptions(); + conversation.setHasMessagesLeftOnServer(false); + updateConversationUi(); } } @@ -1437,7 +1828,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (conversation.getMode() == Conversation.MODE_MULTI) { conversation.getMucOptions().setPassword(password); if (conversation.getBookmark() != null) { - conversation.getBookmark().setAutojoin(true); + if (respectAutojoin()) { + conversation.getBookmark().setAutojoin(true); + } pushBookmarks(conversation.getAccount()); } databaseBackend.updateConversation(conversation); @@ -1495,10 +1888,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void leaveMuc(Conversation conversation) { + leaveMuc(conversation, false); + } + + private void leaveMuc(Conversation conversation, boolean now) { Account account = conversation.getAccount(); account.pendingConferenceJoins.remove(conversation); account.pendingConferenceLeaves.remove(conversation); - if (account.getStatus() == Account.State.ONLINE) { + if (account.getStatus() == Account.State.ONLINE || now) { PresencePacket packet = new PresencePacket(); packet.setTo(conversation.getJid()); packet.setFrom(conversation.getAccount().getJid()); @@ -1546,34 +1943,37 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa 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() { + joinMuc(conversation, new OnConferenceJoined() { @Override - public void onPushSucceeded() { - for (Jid invite : jids) { - invite(conversation, invite); - } - if (account.countPresences() > 1) { - directInvite(conversation, account.getJid().toBareJid()); - } - if (callback != null) { - callback.success(conversation); - } - } + public void onConferenceJoined(final Conversation 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 (account.countPresences() > 1) { + directInvite(conversation, account.getJid().toBareJid()); + } + if (callback != null) { + callback.success(conversation); + } + } - @Override - public void onPushFailed() { - if (callback != null) { - callback.error(R.string.conference_creation_failed, 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); @@ -1587,15 +1987,20 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void fetchConferenceConfiguration(final Conversation conversation) { + fetchConferenceConfiguration(conversation, null); + } + + public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) { 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) { + if (packet.getType() == IqPacket.TYPE.RESULT) { ArrayList<String> features = new ArrayList<>(); - for (Element child : packet.query().getChildren()) { + Element query = packet.query(); + for (Element child : query.getChildren()) { if (child != null && child.getName().equals("feature")) { String var = child.getAttribute("var"); if (var != null) { @@ -1603,8 +2008,19 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } } + Element form = query.findChild("x", "jabber:x:data"); + if (form != null) { + conversation.getMucOptions().updateFormData(Data.parse(form)); + } conversation.getMucOptions().updateFeatures(features); + if (callback != null) { + callback.onConferenceConfigurationFetched(conversation); + } updateConversationUi(); + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + if (callback != null) { + callback.onFetchFailed(conversation, packet.getError()); + } } } }); @@ -1617,11 +2033,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.ERROR) { + if (packet.getType() == IqPacket.TYPE.RESULT) { 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())); + if (options.containsKey(field.getFieldName())) { + field.setValue(options.getString(field.getFieldName())); } } data.submit(); @@ -1631,12 +2047,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa sendIqPacket(account, set, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - if (callback != null) { + if (callback != null) { + if (packet.getType() == IqPacket.TYPE.RESULT) { callback.onPushSucceeded(); - } - } else { - if (callback != null) { + } else { callback.onPushFailed(); } } @@ -1705,7 +2119,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa }); } - public void disconnect(Account account, boolean force) { + private void disconnect(Account account, boolean force) { if ((account.getStatus() == Account.State.ONLINE) || (account.getStatus() == Account.State.DISABLED)) { if (!force) { @@ -1713,7 +2127,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for (Conversation conversation : conversations) { if (conversation.getAccount() == account) { if (conversation.getMode() == Conversation.MODE_MULTI) { - leaveMuc(conversation); + leaveMuc(conversation, true); } else { if (conversation.endOtrIfNeeded()) { Logging.d(Config.LOGTAG, account.getJid().toBareJid() @@ -1739,6 +2153,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa updateConversationUi(); } + public void updateMessage(Message message, String uuid) { + databaseBackend.updateMessage(message, uuid); + updateConversationUi(); + } + protected void syncDirtyContacts(Account account) { for (Contact contact : account.getRoster().getContacts()) { if (contact.getOption(Contact.Options.DIRTY_PUSH)) { @@ -1765,7 +2184,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa account.getJid().toBareJid() + " otr session established with " + conversation.getJid() + "/" + otrSession.getSessionID().getUserID()); - conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() { @Override public void onMessageFound(Message message) { @@ -1778,8 +2197,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (message.needsUploading()) { mJingleConnectionManager.createNewConnection(message); } else { - MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true); + MessagePacket outPacket = mMessageGenerator.generateOtrChat(message); if (outPacket != null) { + mMessageGenerator.addDelay(outPacket, message.getTimeSent()); message.setStatus(Message.STATUS_SEND); databaseBackend.updateMessage(message); sendMessagePacket(account, outPacket); @@ -1799,8 +2219,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa 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"); + MessageGenerator.addMessageHints(packet); packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/" + otrSession.getSessionID().getUserID()); try { @@ -1858,24 +2277,39 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa this.databaseBackend.updateConversation(conversation); } - public void reconnectAccount(final Account account, final boolean force) { + private void reconnectAccount(final Account account, final boolean force, final boolean interactive) { synchronized (account) { - if (account.getXmppConnection() != null) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { disconnect(account, force); + } else { + connection = createConnection(account); + account.setXmppConnection(connection); } if (!account.isOptionSet(Account.OPTION_DISABLED)) { - - AvatarService.getInstance().clearFetchInProgress(account); - - if (account.getXmppConnection() == null) { - account.setXmppConnection(createConnection(account)); + synchronized (this.mInProgressAvatarFetches) { + for (Iterator<String> iterator = this.mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) { + final String KEY = iterator.next(); + if (KEY.startsWith(account.getJid().toBareJid() + "_")) { + iterator.remove(); + } + } + } + if (!force) { + try { + Logging.d(Config.LOGTAG, "wait for disconnect"); + Thread.sleep(500); //sleep wait for disconnect + } catch (InterruptedException e) { + //ignored + } } - Thread thread = new Thread(account.getXmppConnection()); + Thread thread = new Thread(connection); + connection.setInteractive(interactive); thread.start(); - scheduleWakeUpCall(Config.CONNECT_TIMEOUT, account.getUuid().hashCode()); + scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); } else { account.getRoster().clearPresences(); - account.setXmppConnection(null); + connection.resetEverything(); } } } @@ -1884,7 +2318,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa new Thread(new Runnable() { @Override public void run() { - reconnectAccount(account,false); + reconnectAccount(account, false, true); } }).start(); } @@ -1920,7 +2354,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } for (Conversation conversation : getConversations()) { if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) { - final Message message = conversation.findSentMessageWithUuid(uuid); + final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid); if (message != null) { markMessage(message, status); } @@ -1930,8 +2364,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return null; } - public boolean markMessage(Conversation conversation, String uuid, - int status) { + public boolean markMessage(Conversation conversation, String uuid, int status) { if (uuid == null) { return false; } else { @@ -1956,9 +2389,26 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa updateConversationUi(); } + public SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + } + + public boolean saveEncryptedMessages() { + return !getPreferences().getBoolean("dont_save_encrypted", false); + } + + private boolean respectAutojoin() { + return getPreferences().getBoolean("autojoin", true); + } + + public boolean showExtendedConnectionOptions() { + return getPreferences().getBoolean("show_connection_options", false); + } + public int unreadCount() { int count = 0; - for(Conversation conversation : getConversations()) { + for (Conversation conversation : getConversations()) { count += conversation.unreadCount(); } return count; @@ -1989,6 +2439,20 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) { + boolean rc = false; + if (mOnCaptchaRequested != null) { + DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics(); + Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity), + (int) (captcha.getHeight() * metrics.scaledDensity), false); + + mOnCaptchaRequested.onCaptchaRequested(account, id, data, scaled); + rc = true; + } + + return rc; + } + public void updateBlocklistUi(final OnUpdateBlocklist.Status status) { if (mOnUpdateBlocklist != null) { mOnUpdateBlocklist.OnUpdateBlocklist(status); @@ -2001,6 +2465,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void keyStatusUpdated(AxolotlService.FetchStatus report) { + if (mOnKeyStatusUpdated != null) { + mOnKeyStatusUpdated.onKeyStatusUpdated(report); + } + } + public Account findAccountByJid(final Jid accountJid) { for (Account account : this.accounts) { if (account.getJid().toBareJid().equals(accountJid.toBareJid())) { @@ -2019,10 +2489,24 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return null; } - public void markRead(final Conversation conversation) { + public boolean markRead(final Conversation conversation) { mNotificationService.clear(conversation); - conversation.markRead(); - updateUnreadCountBadge(); + final List<Message> readMessages = conversation.markRead(); + if (readMessages.size() > 0) { + Runnable runnable = new Runnable() { + @Override + public void run() { + for (Message message : readMessages) { + databaseBackend.updateMessage(message); + } + } + }; + ConversationsPlusApplication.executeDatabaseOperation(runnable); + updateUnreadCountBadge(); + return true; + } else { + return false; + } } public synchronized void updateUnreadCountBadge() { @@ -2040,7 +2524,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void sendReadMarker(final Conversation conversation) { final Message markable = conversation.getLatestMarkableMessage(); - this.markRead(conversation); + if (this.markRead(conversation)) { + updateConversationUi(); + } if (Settings.CONFIRM_MESSAGE_READ && markable != null && markable.getRemoteMsgId() != null) { Logging.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); Account account = conversation.getAccount(); @@ -2048,7 +2534,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId()); this.sendMessagePacket(conversation.getAccount(), packet); } - updateConversationUi(); } public SecureRandom getRNG() { @@ -2077,6 +2562,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return this.pm; } + public LruCache<String, Bitmap> getBitmapCache() { + return this.mBitmapCache; + } + public void syncRosterToDisk(final Account account) { Runnable runnable = new Runnable() { @@ -2119,27 +2608,50 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return mucServers; } + @Deprecated public void sendMessagePacket(Account account, MessagePacket packet) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendMessagePacket(packet); - } + XmppSendUtil.sendMessagePacket(account, packet); } + @Deprecated public void sendPresencePacket(Account account, PresencePacket packet) { XmppSendUtil.sendPresencePacket(account, packet); } + public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendCaptchaRegistryRequest(id, data); + } + } + + @Deprecated public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) { XmppSendUtil.sendIqPacket(account, packet, callback); } public void sendPresence(final Account account) { - sendPresencePacket(account, mPresenceGenerator.sendPresence(account)); + XmppSendUtil.sendPresencePacket(account, mPresenceGenerator.selfPresence(account, getTargetPresence())); + } + + public void refreshAllPresences() { + for (Account account : getAccounts()) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + sendPresence(account); + } + } + } + + private void refreshAllGcmTokens() { + for(Account account : getAccounts()) { + if (account.isOnlineAndConnected() && mPushManagementService.available(account)) { + mPushManagementService.registerPushTokenOnServer(account); + } + } } public void sendOfflinePresence(final Account account) { - sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); + XmppSendUtil.sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); } public MessageGenerator getMessageGenerator() { @@ -2199,8 +2711,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } for (final Message msg : messages) { + msg.setTime(System.currentTimeMillis()); markMessage(msg, Message.STATUS_WAITING); - this.resendMessage(msg); + this.resendMessage(msg, false); } } @@ -2212,12 +2725,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa * Therefore set this flag to true and try to get messages from server */ conversation.setHasMessagesLeftOnServer(true); - new Thread(new Runnable() { + Runnable runnable = new Runnable() { @Override public void run() { databaseBackend.deleteMessagesInConversation(conversation); } - }).start(); + }; + ConversationsPlusApplication.executeDatabaseOperation(runnable); } public void sendBlockRequest(final Blockable blockable) { @@ -2251,10 +2765,116 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void publishDisplayName(Account account) { + String displayName = account.getDisplayName(); + if (displayName != null && !displayName.isEmpty()) { + IqPacket publish = mIqGenerator.publishNick(displayName); + sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.ERROR) { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not publish nick"); + } + } + }); + } + } + + private ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String,String> key) { + ServiceDiscoveryResult result = discoCache.get(key); + if (result != null) { + return result; + } else { + result = databaseBackend.findDiscoveryResult(key.first, key.second); + if (result != null) { + discoCache.put(key, result); + } + return result; + } + } + + public void fetchCaps(Account account, final Jid jid, final Presence presence) { + final Pair<String,String> key = new Pair<>(presence.getHash(), presence.getVer()); + ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key); + if (disco != null) { + presence.setServiceDiscoveryResult(disco); + } else { + if (!account.inProgressDiscoFetches.contains(key)) { + account.inProgressDiscoFetches.add(key); + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(jid); + request.query("http://jabber.org/protocol/disco#info"); + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": making disco request for "+key.second+" to "+jid); + sendIqPacket(account, request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket discoPacket) { + if (discoPacket.getType() == IqPacket.TYPE.RESULT) { + ServiceDiscoveryResult disco = new ServiceDiscoveryResult(discoPacket); + if (presence.getVer().equals(disco.getVer())) { + databaseBackend.insertDiscoveryResult(disco); + injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco); + } else { + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer()); + } + } + account.inProgressDiscoFetches.remove(key); + } + }); + } + } + } + + private void injectServiceDiscorveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) { + for(Contact contact : roster.getContacts()) { + for(Presence presence : contact.getPresences().getPresences().values()) { + if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) { + presence.setServiceDiscoveryResult(disco); + } + } + } + } + + public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) { + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.addChild("prefs","urn:xmpp:mam:0"); + sendIqPacket(account, request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element prefs = packet.findChild("prefs","urn:xmpp:mam:0"); + if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) { + callback.onPreferencesFetched(prefs); + } else { + callback.onPreferencesFetchFailed(); + } + } + }); + } + + public PushManagementService getPushManagementService() { + return mPushManagementService; + } + + public interface OnMamPreferencesFetched { + void onPreferencesFetched(Element prefs); + void onPreferencesFetchFailed(); + } + + public void pushMamPreferences(Account account, Element prefs) { + IqPacket set = new IqPacket(IqPacket.TYPE.SET); + set.addChild(prefs); + sendIqPacket(account, set, null); + } + + public interface OnAccountCreated { + void onAccountCreated(Account account); + + void informUser(int r); + } + public interface OnMoreMessagesLoaded { - public void onMoreMessagesLoaded(int count, Conversation conversation); + void onMoreMessagesLoaded(int count, Conversation conversation); - public void informUser(int r); + void informUser(int r); void setLoadingInProgress(); @@ -2262,43 +2882,60 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public interface OnAccountPasswordChanged { - public void onPasswordChangeSucceeded(); + void onPasswordChangeSucceeded(); - public void onPasswordChangeFailed(); + void onPasswordChangeFailed(); } public interface OnAffiliationChanged { - public void onAffiliationChangedSuccessful(Jid jid); + void onAffiliationChangedSuccessful(Jid jid); - public void onAffiliationChangeFailed(Jid jid, int resId); + void onAffiliationChangeFailed(Jid jid, int resId); } public interface OnRoleChanged { - public void onRoleChangedSuccessful(String nick); + void onRoleChangedSuccessful(String nick); - public void onRoleChangeFailed(String nick, int resid); + void onRoleChangeFailed(String nick, int resid); } public interface OnConversationUpdate { - public void onConversationUpdate(); + void onConversationUpdate(); } public interface OnAccountUpdate { - public void onAccountUpdate(); + void onAccountUpdate(); + } + + public interface OnCaptchaRequested { + void onCaptchaRequested(Account account, + String id, + Data data, + Bitmap captcha); } public interface OnRosterUpdate { - public void onRosterUpdate(); + void onRosterUpdate(); } public interface OnMucRosterUpdate { - public void onMucRosterUpdate(); + void onMucRosterUpdate(); + } + + public interface OnConferenceConfigurationFetched { + void onConferenceConfigurationFetched(Conversation conversation); + + void onFetchFailed(Conversation conversation, Element error); + } + + public interface OnConferenceJoined { + void onConferenceJoined(Conversation conversation); } public interface OnConferenceOptionsPushed { - public void onPushSucceeded(); + void onPushSucceeded(); - public void onPushFailed(); + void onPushFailed(); } public interface OnShowErrorToast { |