diff --git a/CHANGELOG.md b/CHANGELOG.md index c74a98a73..f9e9d087c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.5.12 +* Jingle file transfer fixes +* Fixed OMEMO self healing (after backup restore) on servers w/o MAM + ### Version 2.5.11 * Fixed crash on Android <5.0 diff --git a/build.gradle b/build.gradle index 133dc5954..da8d493bb 100644 --- a/build.gradle +++ b/build.gradle @@ -84,8 +84,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 25 - versionCode 342 - versionName "2.5.11.1" + versionCode 346 + versionName "2.5.12" archivesBaseName += "-$versionName" applicationId "eu.sum7.conversations" resValue "string", "applicationId", applicationId diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index e138a80ae..36cd26425 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -41,7 +41,7 @@ public final class Config { public static final String MAGIC_CREATE_DOMAIN = "chat.sum7.eu"; public static final String QUICKSY_DOMAIN = "quicksy.im"; - public static final String CHANNEL_DISCOVERY = "https://search.jabbercat.org"; + public static final String CHANNEL_DISCOVERY = "https://search.jabber.network"; public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox @@ -99,6 +99,7 @@ public final class Config { public static final boolean OMEMO_PADDING = false; public static final boolean PUT_AUTH_TAG_INTO_KEY = true; + public static final boolean USE_BOOKMARKS2 = false; public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index b804f4222..59ff20fe5 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -839,6 +839,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { }); } + public void deleteOmemoIdentity() { + final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); + final IqPacket deleteBundleNode = mXmppConnectionService.getIqGenerator().deleteNode(node); + mXmppConnectionService.sendIqPacket(account, deleteBundleNode, null); + publishDeviceIdsAndRefineAccessModel(getOwnDeviceIds()); + } + public List getCryptoTargets(Conversation conversation) { final List jids; if (conversation.getMode() == Conversation.MODE_SINGLE) { diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 91a08a0cd..ef180ded9 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -11,8 +11,10 @@ import org.json.JSONObject; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; @@ -84,7 +86,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private PgpDecryptionService pgpDecryptionService = null; private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; - private List bookmarks = new CopyOnWriteArrayList<>(); + private final Map bookmarks = new HashMap<>(); private Presence.Status presenceStatus = Presence.Status.ONLINE; private String presenceStatusMessage = null; @@ -469,36 +471,51 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return this.roster; } - public List getBookmarks() { - return this.bookmarks; + public Collection getBookmarks() { + return this.bookmarks.values(); } - public void setBookmarks(final CopyOnWriteArrayList bookmarks) { - this.bookmarks = bookmarks; + public void setBookmarks(Map bookmarks) { + synchronized (this.bookmarks) { + this.bookmarks.clear(); + this.bookmarks.putAll(bookmarks); + } + } + + public void putBookmark(Bookmark bookmark) { + synchronized (this.bookmarks) { + this.bookmarks.put(bookmark.getJid(), bookmark); + } + } + + public void removeBookmark(Bookmark bookmark) { + synchronized (this.bookmarks) { + this.bookmarks.remove(bookmark.getJid()); + } + } + + public void removeBookmark(Jid jid) { + synchronized (this.bookmarks) { + this.bookmarks.remove(jid); + } } public Set getBookmarkedJids() { - final Set jids = new HashSet<>(); - for(final Bookmark bookmark : this.bookmarks) { - final Jid jid = bookmark.getJid(); - if (jid != null) { - jids.add(jid.asBareJid()); - } + synchronized (this.bookmarks) { + return new HashSet<>(this.bookmarks.keySet()); } - return jids; } - public boolean hasBookmarkFor(final Jid conferenceJid) { - return getBookmark(conferenceJid) != null; + public boolean hasBookmarkFor(final Jid jid) { + synchronized (this.bookmarks) { + return this.bookmarks.containsKey(jid.asBareJid()); + } } Bookmark getBookmark(final Jid jid) { - for (final Bookmark bookmark : this.bookmarks) { - if (bookmark.getJid() != null && jid.asBareJid().equals(bookmark.getJid().asBareJid())) { - return bookmark; - } + synchronized (this.bookmarks) { + return this.bookmarks.get(jid.asBareJid()); } - return null; } public boolean setAvatar(final String filename) { diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index aa7a1eedb..9d708ce17 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -6,12 +6,16 @@ import android.support.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import eu.siacs.conversations.utils.StringUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.InvalidJid; import rocks.xmpp.addr.Jid; @@ -33,11 +37,69 @@ public class Bookmark extends Element implements ListItem { this.account = account; } + public static Map parseFromStorage(Element storage, Account account) { + if (storage == null) { + return Collections.emptyMap(); + } + final HashMap bookmarks = new HashMap<>(); + for (final Element item : storage.getChildren()) { + if (item.getName().equals("conference")) { + final Bookmark bookmark = Bookmark.parse(item, account); + if (bookmark != null) { + final Bookmark old = bookmarks.put(bookmark.jid, bookmark); + if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) { + bookmark.setBookmarkName(old.getBookmarkName()); + } + } + } + } + return bookmarks; + } + + public static Map parseFromPubsub(Element pubsub, Account account) { + if (pubsub == null) { + return Collections.emptyMap(); + } + final Element items = pubsub.findChild("items"); + if (items != null && Namespace.BOOKMARKS2.equals(items.getAttribute("node"))) { + final Map bookmarks = new HashMap<>(); + for(Element item : items.getChildren()) { + if (item.getName().equals("item")) { + final Bookmark bookmark = Bookmark.parseFromItem(item, account); + if (bookmark != null) { + bookmarks.put(bookmark.jid, bookmark); + } + } + } + return bookmarks; + } + return Collections.emptyMap(); + } + public static Bookmark parse(Element element, Account account) { Bookmark bookmark = new Bookmark(account); bookmark.setAttributes(element.getAttributes()); bookmark.setChildren(element.getChildren()); bookmark.jid = InvalidJid.getNullForInvalid(bookmark.getAttributeAsJid("jid")); + if (bookmark.jid == null) { + return null; + } + return bookmark; + } + + public static Bookmark parseFromItem(Element item, Account account) { + final Element conference = item.findChild("conference", Namespace.BOOKMARKS2); + if (conference == null) { + return null; + } + final Bookmark bookmark = new Bookmark(account); + bookmark.jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("id")); + if (bookmark.jid == null) { + return null; + } + bookmark.setBookmarkName(conference.getAttribute("name")); + bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin")); + bookmark.setNick(conference.findChildContent("nick")); return bookmark; } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index bd0d4bcd2..1267ec415 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -307,8 +307,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl synchronized (this.messages) { for (int i = this.messages.size() - 1; i >= 0; --i) { final Message message = messages.get(i); - if (counterpart.equals(message.getCounterpart()) - && ((message.getStatus() == Message.STATUS_RECEIVED) == received) + final boolean counterpartMatch = mode == MODE_SINGLE ? + counterpart.asBareJid().equals(message.getCounterpart().asBareJid()) : + counterpart.equals(message.getCounterpart()); + if (counterpartMatch && ((message.getStatus() == Message.STATUS_RECEIVED) == received) && (carbon == message.isCarbon() || received)) { final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id); if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) { diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index a70c67198..81463d9e9 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -417,7 +417,7 @@ public class MucOptions { } } - private String getProposedNick() { + public String getProposedNick() { final Bookmark bookmark = this.conversation.getBookmark(); final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick()); if (bookmarkedNick != null) { diff --git a/src/main/java/eu/siacs/conversations/entities/Transferable.java b/src/main/java/eu/siacs/conversations/entities/Transferable.java index df18f3a2d..6a31a368a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Transferable.java +++ b/src/main/java/eu/siacs/conversations/entities/Transferable.java @@ -15,6 +15,7 @@ public interface Transferable { int STATUS_DOWNLOADING = 0x204; int STATUS_OFFER_CHECK_FILESIZE = 0x206; int STATUS_UPLOADING = 0x207; + int STATUS_CANCELLED = 0x208; boolean start(); diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 5f93400e2..df15f39e2 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -19,6 +19,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; public abstract class AbstractGenerator { @@ -38,7 +39,6 @@ public abstract class AbstractGenerator { "http://jabber.org/protocol/disco#info", "urn:xmpp:avatar:metadata+notify", Namespace.NICK+"+notify", - Namespace.BOOKMARKS+"+notify", "urn:xmpp:ping", "jabber:iq:version", "http://jabber.org/protocol/chatstates" @@ -109,7 +109,8 @@ public abstract class AbstractGenerator { } public List getFeatures(Account account) { - ArrayList features = new ArrayList<>(Arrays.asList(FEATURES)); + final XmppConnection connection = account.getXmppConnection(); + final ArrayList features = new ArrayList<>(Arrays.asList(FEATURES)); if (mXmppConnectionService.confirmMessages()) { features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); } @@ -125,6 +126,12 @@ public abstract class AbstractGenerator { if (mXmppConnectionService.broadcastLastActivity()) { features.add(Namespace.IDLE); } + if (connection != null && connection.getFeatures().bookmarks2()) { + features.add(Namespace.BOOKMARKS2 +"+notify"); + } else { + features.add(Namespace.BOOKMARKS+"+notify"); + } + Collections.sort(features); return features; } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index d428e6743..7c7456776 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -38,498 +38,531 @@ import rocks.xmpp.addr.Jid; public class IqGenerator extends AbstractGenerator { - public IqGenerator(final XmppConnectionService service) { - super(service); - } + public IqGenerator(final XmppConnectionService service) { + super(service); + } - public IqPacket discoResponse(final Account account, final IqPacket request) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT); - packet.setId(request.getId()); - packet.setTo(request.getFrom()); - final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info"); - query.setAttribute("node", request.query().getAttribute("node")); - final Element identity = query.addChild("identity"); - identity.setAttribute("category", "client"); - identity.setAttribute("type", getIdentityType()); - identity.setAttribute("name", getIdentityName()); - for (final String feature : getFeatures(account)) { - query.addChild("feature").setAttribute("var", feature); - } - return packet; - } + public IqPacket discoResponse(final Account account, final IqPacket request) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT); + packet.setId(request.getId()); + packet.setTo(request.getFrom()); + final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info"); + query.setAttribute("node", request.query().getAttribute("node")); + final Element identity = query.addChild("identity"); + identity.setAttribute("category", "client"); + identity.setAttribute("type", getIdentityType()); + identity.setAttribute("name", getIdentityName()); + for (final String feature : getFeatures(account)) { + query.addChild("feature").setAttribute("var", feature); + } + return packet; + } - public IqPacket versionResponse(final IqPacket request) { - final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); - Element query = packet.query("jabber:iq:version"); - query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name)); - query.addChild("version").setContent(getIdentityVersion()); - if ("chromium".equals(android.os.Build.BRAND)) { - query.addChild("os").setContent("Chrome OS"); - } else { - query.addChild("os").setContent("Android"); - } - return packet; - } + public IqPacket versionResponse(final IqPacket request) { + final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); + Element query = packet.query("jabber:iq:version"); + query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name)); + query.addChild("version").setContent(getIdentityVersion()); + if ("chromium".equals(android.os.Build.BRAND)) { + query.addChild("os").setContent("Chrome OS"); + } else { + query.addChild("os").setContent("Android"); + } + return packet; + } - public IqPacket entityTimeResponse(IqPacket request) { - final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); - Element time = packet.addChild("time", "urn:xmpp:time"); - final long now = System.currentTimeMillis(); - time.addChild("utc").setContent(getTimestamp(now)); - TimeZone ourTimezone = TimeZone.getDefault(); - long offsetSeconds = ourTimezone.getOffset(now) / 1000; - long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60); - long offsetHours = offsetSeconds / 3600; - String hours; - if (offsetHours < 0) { - hours = String.format(Locale.US, "%03d", offsetHours); - } else { - hours = String.format(Locale.US, "%02d", offsetHours); - } - String minutes = String.format(Locale.US, "%02d", offsetMinutes); - time.addChild("tzo").setContent(hours + ":" + minutes); - return packet; - } + public IqPacket entityTimeResponse(IqPacket request) { + final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); + Element time = packet.addChild("time", "urn:xmpp:time"); + final long now = System.currentTimeMillis(); + time.addChild("utc").setContent(getTimestamp(now)); + TimeZone ourTimezone = TimeZone.getDefault(); + long offsetSeconds = ourTimezone.getOffset(now) / 1000; + long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60); + long offsetHours = offsetSeconds / 3600; + String hours; + if (offsetHours < 0) { + hours = String.format(Locale.US, "%03d", offsetHours); + } else { + hours = String.format(Locale.US, "%02d", offsetHours); + } + String minutes = String.format(Locale.US, "%02d", offsetMinutes); + time.addChild("tzo").setContent(hours + ":" + minutes); + return packet; + } - public IqPacket purgeOfflineMessages() { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge"); - return packet; - } + public IqPacket purgeOfflineMessages() { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge"); + return packet; + } - protected IqPacket publish(final String node, final Element item, final Bundle options) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); - final Element publish = pubsub.addChild("publish"); - publish.setAttribute("node", node); - publish.addChild(item); - if (options != null) { - final Element publishOptions = pubsub.addChild("publish-options"); - publishOptions.addChild(Data.create(Namespace.PUBSUB_PUBLISH_OPTIONS, options)); - } - return packet; - } + protected IqPacket publish(final String node, final Element item, final Bundle options) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); + final Element publish = pubsub.addChild("publish"); + publish.setAttribute("node", node); + publish.addChild(item); + if (options != null) { + final Element publishOptions = pubsub.addChild("publish-options"); + publishOptions.addChild(Data.create(Namespace.PUBSUB_PUBLISH_OPTIONS, options)); + } + return packet; + } - protected IqPacket publish(final String node, final Element item) { - return publish(node, item, null); - } + protected IqPacket publish(final String node, final Element item) { + return publish(node, item, null); + } - private IqPacket retrieve(String node, Element item) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); - final Element items = pubsub.addChild("items"); - items.setAttribute("node", node); - if (item != null) { - items.addChild(item); - } - return packet; - } + private IqPacket retrieve(String node, Element item) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); + final Element items = pubsub.addChild("items"); + items.setAttribute("node", node); + if (item != null) { + items.addChild(item); + } + return packet; + } - public IqPacket publishNick(String nick) { - final Element item = new Element("item"); - item.addChild("nick", Namespace.NICK).setContent(nick); - return publish(Namespace.NICK, item); - } + public IqPacket retrieveBookmarks() { + return retrieve(Namespace.BOOKMARKS2, null); + } - public IqPacket deleteNode(String node) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER); - pubsub.addChild("delete").setAttribute("node",node); - return packet; - } + public IqPacket publishNick(String nick) { + final Element item = new Element("item"); + item.addChild("nick", Namespace.NICK).setContent(nick); + return publish(Namespace.NICK, item); + } - public IqPacket publishAvatar(Avatar avatar, Bundle options) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element data = item.addChild("data", "urn:xmpp:avatar:data"); - data.setContent(avatar.image); - return publish("urn:xmpp:avatar:data", item, options); - } + public IqPacket deleteNode(String node) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER); + pubsub.addChild("delete").setAttribute("node", node); + return packet; + } - public IqPacket publishElement(final String namespace,final Element element, final Bundle options) { - final Element item = new Element("item"); - item.setAttribute("id","current"); - item.addChild(element); - return publish(namespace, item, options); - } + public IqPacket deleteItem(final String node, final String id) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); + final Element retract = pubsub.addChild("retract"); + retract.setAttribute("node", node); + retract.setAttribute("notify","true"); + retract.addChild("item").setAttribute("id", id); + return packet; + } - public IqPacket publishAvatarMetadata(final Avatar avatar, final Bundle options) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element metadata = item - .addChild("metadata", "urn:xmpp:avatar:metadata"); - final Element info = metadata.addChild("info"); - info.setAttribute("bytes", avatar.size); - info.setAttribute("id", avatar.sha1sum); - info.setAttribute("height", avatar.height); - info.setAttribute("width", avatar.height); - info.setAttribute("type", avatar.type); - return publish("urn:xmpp:avatar:metadata", item, options); - } + public IqPacket publishAvatar(Avatar avatar, Bundle options) { + final Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + final Element data = item.addChild("data", "urn:xmpp:avatar:data"); + data.setContent(avatar.image); + return publish("urn:xmpp:avatar:data", item, options); + } - public IqPacket retrievePepAvatar(final Avatar avatar) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final IqPacket packet = retrieve("urn:xmpp:avatar:data", item); - packet.setTo(avatar.owner); - return packet; - } + public IqPacket publishElement(final String namespace, final Element element, final Bundle options) { + return publishElement(namespace, element, "curent", options); + } - public IqPacket retrieveVcardAvatar(final Avatar avatar) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(avatar.owner); - packet.addChild("vCard", "vcard-temp"); - return packet; - } + public IqPacket publishElement(final String namespace, final Element element, String id, final Bundle options) { + final Element item = new Element("item"); + item.setAttribute("id", id); + item.addChild(element); + return publish(namespace, item, options); + } - public IqPacket retrieveAvatarMetaData(final Jid to) { - final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); - if (to != null) { - packet.setTo(to); - } - return packet; - } + public IqPacket publishAvatarMetadata(final Avatar avatar, final Bundle options) { + final Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + final Element metadata = item + .addChild("metadata", "urn:xmpp:avatar:metadata"); + final Element info = metadata.addChild("info"); + info.setAttribute("bytes", avatar.size); + info.setAttribute("id", avatar.sha1sum); + info.setAttribute("height", avatar.height); + info.setAttribute("width", avatar.height); + info.setAttribute("type", avatar.type); + return publish("urn:xmpp:avatar:metadata", item, options); + } - public IqPacket retrieveDeviceIds(final Jid to) { - final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); - if (to != null) { - packet.setTo(to); - } - return packet; - } + public IqPacket retrievePepAvatar(final Avatar avatar) { + final Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + final IqPacket packet = retrieve("urn:xmpp:avatar:data", item); + packet.setTo(avatar.owner); + return packet; + } - public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null); - packet.setTo(to); - return packet; - } + public IqPacket retrieveVcardAvatar(final Avatar avatar) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(avatar.owner); + packet.addChild("vCard", "vcard-temp"); + return packet; + } - public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null); - packet.setTo(to); - return packet; - } + public IqPacket retrieveAvatarMetaData(final Jid to) { + final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); + if (to != null) { + packet.setTo(to); + } + return packet; + } - public IqPacket publishDeviceIds(final Set ids, final Bundle publishOptions) { - final Element item = new Element("item"); - item.setAttribute("id", "current"); - final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); - for (Integer id : ids) { - final Element device = new Element("device"); - device.setAttribute("id", id); - list.addChild(device); - } - return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions); - } + public IqPacket retrieveDeviceIds(final Jid to) { + final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); + if (to != null) { + packet.setTo(to); + } + return packet; + } - public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, - final Set preKeyRecords, final int deviceId, Bundle publishOptions) { - final Element item = new Element("item"); - item.setAttribute("id", "current"); - final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); - final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); - signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId()); - ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey(); - signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.DEFAULT)); - final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature"); - signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.DEFAULT)); - final Element identityKeyElement = bundle.addChild("identityKey"); - identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); + public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) { + final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null); + packet.setTo(to); + return packet; + } - final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX); - for (PreKeyRecord preKeyRecord : preKeyRecords) { - final Element prekey = prekeys.addChild("preKeyPublic"); - prekey.setAttribute("preKeyId", preKeyRecord.getId()); - prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); - } + public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) { + final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null); + packet.setTo(to); + return packet; + } - return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions); - } + public IqPacket publishDeviceIds(final Set ids, final Bundle publishOptions) { + final Element item = new Element("item"); + item.setAttribute("id", "current"); + final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); + for (Integer id : ids) { + final Element device = new Element("device"); + device.setAttribute("id", id); + list.addChild(device); + } + return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions); + } - public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) { - final Element item = new Element("item"); - item.setAttribute("id", "current"); - final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX); - final Element chain = verification.addChild("chain"); - for (int i = 0; i < certificates.length; ++i) { - try { - Element certificate = chain.addChild("certificate"); - certificate.setContent(Base64.encodeToString(certificates[i].getEncoded(), Base64.DEFAULT)); - certificate.setAttribute("index", i); - } catch (CertificateEncodingException e) { - Log.d(Config.LOGTAG, "could not encode certificate"); - } - } - verification.addChild("signature").setContent(Base64.encodeToString(signature, Base64.DEFAULT)); - return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item); - } + public Element publishBookmarkItem(final Bookmark bookmark) { + final String name = bookmark.getBookmarkName(); + final String nick = bookmark.getNick(); + final boolean autojoin = bookmark.autojoin(); + final Element conference = new Element("conference", Namespace.BOOKMARKS2); + if (name != null) { + conference.setAttribute("name", name); + } + if (nick != null) { + conference.addChild("nick").setContent(nick); + } + conference.setAttribute("autojoin",String.valueOf(autojoin)); + return conference; + } - public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element query = packet.query(mam.version.namespace); - query.setAttribute("queryid", mam.getQueryId()); - final Data data = new Data(); - data.setFormType(mam.version.namespace); - if (mam.muc()) { - packet.setTo(mam.getWith()); - } else if (mam.getWith() != null) { - data.put("with", mam.getWith().toString()); - } - final long start = mam.getStart(); - final long end = mam.getEnd(); - if (start != 0) { - data.put("start", getTimestamp(start)); - } - if (end != 0) { - data.put("end", getTimestamp(end)); - } - data.submit(); - query.addChild(data); - Element set = query.addChild("set", "http://jabber.org/protocol/rsm"); - if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { - set.addChild("before").setContent(mam.getReference()); - } else if (mam.getReference() != null) { - set.addChild("after").setContent(mam.getReference()); - } - set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE)); - return packet; - } + public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, + final Set preKeyRecords, final int deviceId, Bundle publishOptions) { + final Element item = new Element("item"); + item.setAttribute("id", "current"); + final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); + final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); + signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId()); + ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey(); + signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.DEFAULT)); + final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature"); + signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.DEFAULT)); + final Element identityKeyElement = bundle.addChild("identityKey"); + identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); - public IqPacket generateGetBlockList() { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.addChild("blocklist", Namespace.BLOCKING); + final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX); + for (PreKeyRecord preKeyRecord : preKeyRecords) { + final Element prekey = prekeys.addChild("preKeyPublic"); + prekey.setAttribute("preKeyId", preKeyRecord.getId()); + prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); + } - return iq; - } + return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions); + } - public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - final Element block = iq.addChild("block", Namespace.BLOCKING); - final Element item = block.addChild("item").setAttribute("jid", jid.toEscapedString()); - if (reportSpam) { - item.addChild("report", "urn:xmpp:reporting:0").addChild("spam"); - } - Log.d(Config.LOGTAG, iq.toString()); - return iq; - } + public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) { + final Element item = new Element("item"); + item.setAttribute("id", "current"); + final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX); + final Element chain = verification.addChild("chain"); + for (int i = 0; i < certificates.length; ++i) { + try { + Element certificate = chain.addChild("certificate"); + certificate.setContent(Base64.encodeToString(certificates[i].getEncoded(), Base64.DEFAULT)); + certificate.setAttribute("index", i); + } catch (CertificateEncodingException e) { + Log.d(Config.LOGTAG, "could not encode certificate"); + } + } + verification.addChild("signature").setContent(Base64.encodeToString(signature, Base64.DEFAULT)); + return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item); + } - public IqPacket generateSetUnblockRequest(final Jid jid) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - final Element block = iq.addChild("unblock", Namespace.BLOCKING); - block.addChild("item").setAttribute("jid", jid.toEscapedString()); - return iq; - } + public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final Element query = packet.query(mam.version.namespace); + query.setAttribute("queryid", mam.getQueryId()); + final Data data = new Data(); + data.setFormType(mam.version.namespace); + if (mam.muc()) { + packet.setTo(mam.getWith()); + } else if (mam.getWith() != null) { + data.put("with", mam.getWith().toString()); + } + final long start = mam.getStart(); + final long end = mam.getEnd(); + if (start != 0) { + data.put("start", getTimestamp(start)); + } + if (end != 0) { + data.put("end", getTimestamp(end)); + } + data.submit(); + query.addChild(data); + Element set = query.addChild("set", "http://jabber.org/protocol/rsm"); + if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { + set.addChild("before").setContent(mam.getReference()); + } else if (mam.getReference() != null) { + set.addChild("after").setContent(mam.getReference()); + } + set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE)); + return packet; + } - public IqPacket generateSetPassword(final Account account, final String newPassword) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(Jid.of(account.getServer())); - final Element query = packet.addChild("query", Namespace.REGISTER); - final Jid jid = account.getJid(); - query.addChild("username").setContent(jid.getLocal()); - query.addChild("password").setContent(newPassword); - return packet; - } + public IqPacket generateGetBlockList() { + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.addChild("blocklist", Namespace.BLOCKING); - public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) { - List jids = new ArrayList<>(); - jids.add(jid); - return changeAffiliation(conference, jids, affiliation); - } + return iq; + } - public IqPacket changeAffiliation(Conversation conference, List jids, String affiliation) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(conference.getJid().asBareJid()); - packet.setFrom(conference.getAccount().getJid()); - Element query = packet.query("http://jabber.org/protocol/muc#admin"); - for (Jid jid : jids) { - Element item = query.addChild("item"); - item.setAttribute("jid", jid.toEscapedString()); - item.setAttribute("affiliation", affiliation); - } - return packet; - } + public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) { + final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Element block = iq.addChild("block", Namespace.BLOCKING); + final Element item = block.addChild("item").setAttribute("jid", jid.toEscapedString()); + if (reportSpam) { + item.addChild("report", "urn:xmpp:reporting:0").addChild("spam"); + } + Log.d(Config.LOGTAG, iq.toString()); + return iq; + } - public IqPacket changeRole(Conversation conference, String nick, String role) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(conference.getJid().asBareJid()); - packet.setFrom(conference.getAccount().getJid()); - Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item"); - item.setAttribute("nick", nick); - item.setAttribute("role", role); - return packet; - } + public IqPacket generateSetUnblockRequest(final Jid jid) { + final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Element block = iq.addChild("unblock", Namespace.BLOCKING); + block.addChild("item").setAttribute("jid", jid.toEscapedString()); + return iq; + } - public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(host); - Element request = packet.addChild("request", Namespace.HTTP_UPLOAD); - request.setAttribute("filename", convertFilename(file.getName())); - request.setAttribute("size", file.getExpectedSize()); - request.setAttribute("content-type", mime); - return packet; - } + public IqPacket generateSetPassword(final Account account, final String newPassword) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(Jid.of(account.getServer())); + final Element query = packet.addChild("query", Namespace.REGISTER); + final Jid jid = account.getJid(); + query.addChild("username").setContent(jid.getLocal()); + query.addChild("password").setContent(newPassword); + return packet; + } - public IqPacket requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(host); - Element request = packet.addChild("request", Namespace.HTTP_UPLOAD_LEGACY); - request.addChild("filename").setContent(convertFilename(file.getName())); - request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); - request.addChild("content-type").setContent(mime); - return packet; - } + public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) { + List jids = new ArrayList<>(); + jids.add(jid); + return changeAffiliation(conference, jids, affiliation); + } - public IqPacket requestP1S3Slot(Jid host, String md5) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(host); - packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5", md5); - return packet; - } + public IqPacket changeAffiliation(Conversation conference, List jids, String affiliation) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(conference.getJid().asBareJid()); + packet.setFrom(conference.getAccount().getJid()); + Element query = packet.query("http://jabber.org/protocol/muc#admin"); + for (Jid jid : jids) { + Element item = query.addChild("item"); + item.setAttribute("jid", jid.toEscapedString()); + item.setAttribute("affiliation", affiliation); + } + return packet; + } - public IqPacket requestP1S3Url(Jid host, String fileId) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(host); - packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("fileid", fileId); - return packet; - } + public IqPacket changeRole(Conversation conference, String nick, String role) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(conference.getJid().asBareJid()); + packet.setFrom(conference.getAccount().getJid()); + Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item"); + item.setAttribute("nick", nick); + item.setAttribute("role", role); + return packet; + } - private static String convertFilename(String name) { - int pos = name.indexOf('.'); - if (pos != -1) { - try { - UUID uuid = UUID.fromString(name.substring(0, pos)); - ByteBuffer bb = ByteBuffer.wrap(new byte[16]); - bb.putLong(uuid.getMostSignificantBits()); - bb.putLong(uuid.getLeastSignificantBits()); - return Base64.encodeToString(bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP) + name.substring(pos, name.length()); - } catch (Exception e) { - return name; - } - } else { - return name; - } - } + public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) { + IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(host); + Element request = packet.addChild("request", Namespace.HTTP_UPLOAD); + request.setAttribute("filename", convertFilename(file.getName())); + request.setAttribute("size", file.getExpectedSize()); + request.setAttribute("content-type", mime); + return packet; + } - public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) { - final IqPacket register = new IqPacket(IqPacket.TYPE.SET); - register.setFrom(account.getJid().asBareJid()); - register.setTo(Jid.of(account.getServer())); - register.setId(id); - Element query = register.query(Namespace.REGISTER); - if (data != null) { - query.addChild(data); - } - return register; - } + public IqPacket requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) { + IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(host); + Element request = packet.addChild("request", Namespace.HTTP_UPLOAD_LEGACY); + request.addChild("filename").setContent(convertFilename(file.getName())); + request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); + request.addChild("content-type").setContent(mime); + return packet; + } - public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) { - return pushTokenToAppServer(appServer, token, deviceId, null); - } + public IqPacket requestP1S3Slot(Jid host, String md5) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(host); + packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("md5", md5); + return packet; + } - public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(appServer); - final Element command = packet.addChild("command", Namespace.COMMANDS); - command.setAttribute("node", "register-push-fcm"); - command.setAttribute("action", "execute"); - final Data data = new Data(); - data.put("token", token); - data.put("android-id", deviceId); - if (muc != null) { - data.put("muc", muc.toEscapedString()); - } - data.submit(); - command.addChild(data); - return packet; - } + public IqPacket requestP1S3Url(Jid host, String fileId) { + IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(host); + packet.query(Namespace.P1_S3_FILE_TRANSFER).setAttribute("fileid", fileId); + return packet; + } - public IqPacket unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(appServer); - final Element command = packet.addChild("command", Namespace.COMMANDS); - command.setAttribute("node", "unregister-push-fcm"); - command.setAttribute("action", "execute"); - final Data data = new Data(); - data.put("channel", channel); - data.put("android-id", deviceId); - data.submit(); - command.addChild(data); - return packet; - } + private static String convertFilename(String name) { + int pos = name.indexOf('.'); + if (pos != -1) { + try { + UUID uuid = UUID.fromString(name.substring(0, pos)); + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return Base64.encodeToString(bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP) + name.substring(pos, name.length()); + } catch (Exception e) { + return name; + } + } else { + return name; + } + } - public IqPacket enablePush(final Jid jid, final String node, final String secret) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - Element enable = packet.addChild("enable", Namespace.PUSH); - enable.setAttribute("jid", jid.toString()); - enable.setAttribute("node", node); - if (secret != null) { - Data data = new Data(); - data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS); - data.put("secret", secret); - data.submit(); - enable.addChild(data); - } - return packet; - } + public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) { + final IqPacket register = new IqPacket(IqPacket.TYPE.SET); + register.setFrom(account.getJid().asBareJid()); + register.setTo(Jid.of(account.getServer())); + register.setId(id); + Element query = register.query(Namespace.REGISTER); + if (data != null) { + query.addChild(data); + } + return register; + } - public IqPacket disablePush(final Jid jid, final String node) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - Element disable = packet.addChild("disable", Namespace.PUSH); - disable.setAttribute("jid", jid.toEscapedString()); - disable.setAttribute("node", node); - return packet; - } + public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) { + return pushTokenToAppServer(appServer, token, deviceId, null); + } - public IqPacket queryAffiliation(Conversation conversation, String affiliation) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(conversation.getJid().asBareJid()); - packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation", affiliation); - return packet; - } + public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(appServer); + final Element command = packet.addChild("command", Namespace.COMMANDS); + command.setAttribute("node", "register-push-fcm"); + command.setAttribute("action", "execute"); + final Data data = new Data(); + data.put("token", token); + data.put("android-id", deviceId); + if (muc != null) { + data.put("muc", muc.toEscapedString()); + } + data.submit(); + command.addChild(data); + return packet; + } - public static Bundle defaultGroupChatConfiguration() { - 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"); - options.putString("muc#roomconfig_changesubject", "0"); - options.putString("muc#roomconfig_allowinvites", "0"); - options.putString("muc#roomconfig_enablearchiving", "1"); //prosody - options.putString("mam", "1"); //ejabberd community - options.putString("muc#roomconfig_mam","1"); //ejabberd saas - return options; - } + public IqPacket unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(appServer); + final Element command = packet.addChild("command", Namespace.COMMANDS); + command.setAttribute("node", "unregister-push-fcm"); + command.setAttribute("action", "execute"); + final Data data = new Data(); + data.put("channel", channel); + data.put("android-id", deviceId); + data.submit(); + command.addChild(data); + return packet; + } - public static Bundle defaultChannelConfiguration() { - Bundle options = new Bundle(); - options.putString("muc#roomconfig_persistentroom", "1"); - options.putString("muc#roomconfig_membersonly", "0"); - options.putString("muc#roomconfig_publicroom", "1"); - options.putString("muc#roomconfig_whois", "moderators"); - options.putString("muc#roomconfig_changesubject", "0"); - options.putString("muc#roomconfig_enablearchiving", "1"); //prosody - options.putString("mam", "1"); //ejabberd community - options.putString("muc#roomconfig_mam","1"); //ejabberd saas - return options; - } + public IqPacket enablePush(final Jid jid, final String node, final String secret) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + Element enable = packet.addChild("enable", Namespace.PUSH); + enable.setAttribute("jid", jid.toString()); + enable.setAttribute("node", node); + if (secret != null) { + Data data = new Data(); + data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS); + data.put("secret", secret); + data.submit(); + enable.addChild(data); + } + return packet; + } - public IqPacket requestPubsubConfiguration(Jid jid, String node) { - return pubsubConfiguration(jid, node, null); - } + public IqPacket disablePush(final Jid jid, final String node) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + Element disable = packet.addChild("disable", Namespace.PUSH); + disable.setAttribute("jid", jid.toEscapedString()); + disable.setAttribute("node", node); + return packet; + } - public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) { - return pubsubConfiguration(jid, node, data); - } + public IqPacket queryAffiliation(Conversation conversation, String affiliation) { + IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(conversation.getJid().asBareJid()); + packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation", affiliation); + return packet; + } - private IqPacket pubsubConfiguration(Jid jid, String node, Data data) { - IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET); - packet.setTo(jid); - Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); - Element configure = pubsub.addChild("configure").setAttribute("node", node); - if (data != null) { - configure.addChild(data); - } - return packet; - } + public static Bundle defaultGroupChatConfiguration() { + 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"); + options.putString("muc#roomconfig_changesubject", "0"); + options.putString("muc#roomconfig_allowinvites", "0"); + options.putString("muc#roomconfig_enablearchiving", "1"); //prosody + options.putString("mam", "1"); //ejabberd community + options.putString("muc#roomconfig_mam", "1"); //ejabberd saas + return options; + } + + public static Bundle defaultChannelConfiguration() { + Bundle options = new Bundle(); + options.putString("muc#roomconfig_persistentroom", "1"); + options.putString("muc#roomconfig_membersonly", "0"); + options.putString("muc#roomconfig_publicroom", "1"); + options.putString("muc#roomconfig_whois", "moderators"); + options.putString("muc#roomconfig_changesubject", "0"); + options.putString("muc#roomconfig_enablearchiving", "1"); //prosody + options.putString("mam", "1"); //ejabberd community + options.putString("muc#roomconfig_mam", "1"); //ejabberd saas + return options; + } + + public IqPacket requestPubsubConfiguration(Jid jid, String node) { + return pubsubConfiguration(jid, node, null); + } + + public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) { + return pubsubConfiguration(jid, node, data); + } + + private IqPacket pubsubConfiguration(Jid jid, String node, Data data) { + IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET); + packet.setTo(jid); + Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); + Element configure = pubsub.addChild("configure").setAttribute("node", node); + if (data != null) { + configure.addChild(data); + } + return packet; + } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 1e7aacb1d..b9bedec1a 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -10,6 +10,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.UUID; @@ -20,6 +21,7 @@ import eu.siacs.conversations.crypto.axolotl.BrokenSessionException; import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; @@ -110,7 +112,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return false; } - private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, boolean checkedForDuplicates, boolean postpone) { + private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) { final AxolotlService service = conversation.getAccount().getAxolotlService(); final XmppAxolotlMessage xmppAxolotlMessage; try { @@ -134,7 +136,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } else { Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicates failed"); - //TODO should be still emit a failed message? return null; } } catch (NotEncryptedForThisDeviceException e) { @@ -224,23 +225,55 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (account.getXmppConnection().getFeatures().bookmarksConversion()) { final Element i = items.findChild("item"); final Element storage = i == null ? null : i.findChild("storage", Namespace.BOOKMARKS); - mXmppConnectionService.processBookmarks(account, storage, true); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": processing bookmark PEP event"); + Map bookmarks = Bookmark.parseFromStorage(storage, account); + mXmppConnectionService.processBookmarksInitial(account, bookmarks, true); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": processing bookmark PEP event"); } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring bookmark PEP event because bookmark conversion was not detected"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring bookmark PEP event because bookmark conversion was not detected"); } + } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { + final Element item = items.findChild("item"); + final Element retract = items.findChild("retract"); + if (item != null) { + final Bookmark bookmark = Bookmark.parseFromItem(item, account); + if (bookmark != null) { + account.putBookmark(bookmark); + mXmppConnectionService.processModifiedBookmark(bookmark); + mXmppConnectionService.updateConversationUi(); + } + } + if (retract != null) { + final Jid id = InvalidJid.getNullForInvalid(retract.getAttributeAsJid("id")); + if (id != null) { + account.removeBookmark(id); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted bookmark for "+id); + mXmppConnectionService.processDeletedBookmark(account, id); + mXmppConnectionService.updateConversationUi(); + } + } + } else { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+" received pubsub notification for node="+node); } } private void parseDeleteEvent(final Element event, final Jid from, final Account account) { final Element delete = event.findChild("delete"); - if (delete == null) { - return; - } - String node = delete.getAttribute("node"); + final String node = delete == null ? null : delete.getAttribute("node"); if (Namespace.NICK.equals(node)) { Log.d(Config.LOGTAG, "parsing nick delete event from " + from); setNick(account, from, null); + } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { + account.setBookmarks(Collections.emptyMap()); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted bookmarks node"); + } + } + + private void parsePurgeEvent(final Element event, final Jid from, final Account account) { + final Element purge = event.findChild("purge"); + final String node = purge == null ? null : purge.getAttribute("node"); + if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { + account.setBookmarks(Collections.emptyMap()); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": purged bookmarks"); } } @@ -463,8 +496,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece origin = from; } - //TODO either or is probably fine? - final boolean checkedForDuplicates = serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId); + final boolean liveMessage = query == null && !isTypeGroupChat && mucUserElement == null; + final boolean checkedForDuplicates = liveMessage || (serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId)); if (origin != null) { message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, checkedForDuplicates,query != null); @@ -835,6 +868,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece parseEvent(event, original.getFrom(), account); } else if (event.hasChild("delete")) { parseDeleteEvent(event, original.getFrom(), account); + } else if (event.hasChild("purge")) { + parsePurgeEvent(event, original.getFrom(), account); } } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 509d4b193..37168016a 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -56,6 +56,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AttachFileToConversationRunnable; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.RecordingActivity; import eu.siacs.conversations.ui.util.Attachment; @@ -111,6 +112,7 @@ public class FileBackend { } public static boolean allFilesUnderSize(Context context, List attachments, long max) { + final boolean compressVideo = !AttachFileToConversationRunnable.getVideoCompression(context).equals("uncompressed"); if (max <= 0) { Log.d(Config.LOGTAG, "server did not report max file size for http upload"); return true; //exception to be compatible with HTTP Upload < v0.2 @@ -120,7 +122,7 @@ public class FileBackend { continue; } String mime = attachment.getMime(); - if (mime != null && mime.startsWith("video/")) { + if (mime != null && mime.startsWith("video/") && compressVideo) { try { Dimensions dimensions = FileBackend.getVideoDimensions(context, attachment.getUri()); if (dimensions.getMin() > 720) { @@ -191,6 +193,14 @@ public class FileBackend { return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/"; } + public static Uri getUriForUri(Context context, Uri uri) { + if ("file".equals(uri.getScheme())) { + return getUriForFile(context, new File(uri.getPath())); + } else { + return uri; + } + } + public static Uri getUriForFile(Context context, File file) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) { try { diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java index 21ab5fb00..eb3380396 100644 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.services; +import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; @@ -177,7 +178,11 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod } private String getVideoCompression() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); - return preferences.getString("video_compression", mXmppConnectionService.getResources().getString(R.string.video_compression)); + return getVideoCompression(mXmppConnectionService); + } + + public static String getVideoCompression(final Context context) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression)); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 68db573a9..5eef556e2 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -57,7 +57,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; @@ -298,7 +297,7 @@ public class XmppConnectionService extends Service { if (loggedInSuccessfully) { if (!TextUtils.isEmpty(account.getDisplayName())) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": display name wasn't empty on first log in. publishing"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": display name wasn't empty on first log in. publishing"); publishDisplayName(account); } } @@ -313,7 +312,12 @@ public class XmppConnectionService extends Service { mJingleConnectionManager.cancelInTransmission(); mQuickConversationsService.considerSyncBackground(false); fetchRosterFromServer(account); - if (!account.getXmppConnection().getFeatures().bookmarksConversion()) { + + final XmppConnection connection = account.getXmppConnection(); + + if (connection.getFeatures().bookmarks2()) { + fetchBookmarks2(account); + } else if (!account.getXmppConnection().getFeatures().bookmarksConversion()) { fetchBookmarks(account); } final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval(); @@ -522,8 +526,8 @@ public class XmppConnectionService extends Service { message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_FILE); } - Log.d(Config.LOGTAG,"attachFile: type="+message.getType()); - Log.d(Config.LOGTAG,"counterpart="+message.getCounterpart()); + Log.d(Config.LOGTAG, "attachFile: type=" + message.getType()); + Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart()); final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback); if (runnable.isVideoMessage()) { mVideoCompressionExecutor.execute(runnable); @@ -554,7 +558,7 @@ public class XmppConnectionService extends Service { message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_IMAGE); } - Log.d(Config.LOGTAG,"attachImage: type="+message.getType()); + Log.d(Config.LOGTAG, "attachImage: type=" + message.getType()); mFileAddingExecutor.execute(() -> { try { getFileBackend().copyImageToPrivateStorage(message, uri); @@ -597,7 +601,7 @@ public class XmppConnectionService extends Service { final String action = intent == null ? null : intent.getAction(); final boolean needsForegroundService = intent != null && intent.getBooleanExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false); if (needsForegroundService) { - Log.d(Config.LOGTAG,"toggle forced foreground service after receiving event (action="+action+")"); + Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")"); toggleForegroundService(true); } String pushedAccountHash = null; @@ -833,12 +837,12 @@ public class XmppConnectionService extends Service { } private void checkMucStillJoined(final Account account, final String hash, final String androidId) { - for(final Conversation conversation : this.conversations) { + for (final Conversation conversation : this.conversations) { if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) { Jid jid = conversation.getJid().asBareJid(); final String currentHash = CryptoHelper.getFingerprint(jid, androidId); if (currentHash.equals(hash)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received cloud push notification for MUC "+jid); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received cloud push notification for MUC " + jid); return; } } @@ -1046,7 +1050,7 @@ public class XmppConnectionService extends Service { try { Security.insertProviderAt(Conscrypt.newProvider(), 1); } catch (Throwable throwable) { - Log.e(Config.LOGTAG,"unable to initialize security provider", throwable); + Log.e(Config.LOGTAG, "unable to initialize security provider", throwable); } Resolver.init(this); this.mRandom = new SecureRandom(); @@ -1133,7 +1137,7 @@ public class XmppConnectionService extends Service { final long start = SystemClock.elapsedRealtime(); final List relativeFilePaths = databaseBackend.getFilePathInfo(); final List changed = new ArrayList<>(); - for(final DatabaseBackend.FilePathInfo filePath : relativeFilePaths) { + for (final DatabaseBackend.FilePathInfo filePath : relativeFilePaths) { if (destroyed) { Log.d(Config.LOGTAG, "Stop checking for deleted files because service has been destroyed"); return; @@ -1144,7 +1148,7 @@ public class XmppConnectionService extends Service { } } final long duration = SystemClock.elapsedRealtime() - start; - Log.d(Config.LOGTAG,"found "+changed.size()+" changed files on start up. total="+relativeFilePaths.size()+". ("+duration+"ms)"); + Log.d(Config.LOGTAG, "found " + changed.size() + " changed files on start up. total=" + relativeFilePaths.size() + ". (" + duration + "ms)"); if (changed.size() > 0) { databaseBackend.markFilesAsChanged(changed); markChangedFiles(changed); @@ -1225,7 +1229,7 @@ public class XmppConnectionService extends Service { if (!mForceForegroundService.get()) { mNotificationService.dismissForcedForegroundNotification(); //if the channel was changed the previous call might fail } - Log.d(Config.LOGTAG,"ForegroundService: "+(status?"on":"off")); + Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off")); } public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() { @@ -1365,7 +1369,7 @@ public class XmppConnectionService extends Service { if (QuickConversationsService.isQuicksy() && conversation.getMode() == Conversation.MODE_SINGLE) { final Contact contact = conversation.getContact(); if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": adding "+contact.getJid()+" on sending message"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": adding " + contact.getJid() + " on sending message"); createContact(contact, true); } } @@ -1456,7 +1460,7 @@ public class XmppConnectionService extends Service { message.setBody(decryptedBody); message.setEncryption(Message.ENCRYPTION_DECRYPTED); if (!databaseBackend.updateMessage(message, message.getEditedId())) { - Log.e(Config.LOGTAG,"error updated message in DB after edit"); + Log.e(Config.LOGTAG, "error updated message in DB after edit"); } updateConversationUi(); return; @@ -1496,7 +1500,7 @@ public class XmppConnectionService extends Service { databaseBackend.createMessage(message); } else if (message.edited()) { if (!databaseBackend.updateMessage(message, message.getEditedId())) { - Log.e(Config.LOGTAG,"error updated message in DB after edit"); + Log.e(Config.LOGTAG, "error updated message in DB after edit"); } } updateConversationUi(); @@ -1522,7 +1526,7 @@ public class XmppConnectionService extends Service { final boolean pending = account.pendingConferenceJoins.contains(conversation); final boolean inProgressJoin = inProgress || pending; if (inProgressJoin) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": holding back message to group. inProgress="+inProgress+", pending="+pending); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": holding back message to group. inProgress=" + inProgress + ", pending=" + pending); } return inProgressJoin; } else { @@ -1559,7 +1563,8 @@ public class XmppConnectionService extends Service { if (response.getType() == IqPacket.TYPE.RESULT) { final Element query1 = response.query(); final Element storage = query1.findChild("storage", "storage:bookmarks"); - processBookmarks(a, storage, false); + Map bookmarks = Bookmark.parseFromStorage(storage, account); + processBookmarksInitial(a, bookmarks, false); } else { Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": could not fetch bookmarks"); } @@ -1567,55 +1572,101 @@ public class XmppConnectionService extends Service { sendIqPacket(account, iqPacket, callback); } - public void processBookmarks(Account account, Element storage, final boolean pep) { - final Set previousBookmarks = account.getBookmarkedJids(); - final HashMap bookmarks = new HashMap<>(); - final boolean synchronizeWithBookmarks = synchronizeWithBookmarks(); - 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()); - } - if (bookmark.getJid() == null) { - continue; - } - previousBookmarks.remove(bookmark.getJid().asBareJid()); - Conversation conversation = find(bookmark); - if (conversation != null) { - if (conversation.getMode() != Conversation.MODE_MULTI) { - continue; - } - bookmark.setConversation(conversation); - if (pep && synchronizeWithBookmarks && !bookmark.autojoin()) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": archiving conference ("+conversation.getJid()+") after receiving pep"); - archiveConversation(conversation, false); - } - } else if (synchronizeWithBookmarks && bookmark.autojoin()) { - conversation = findOrCreateConversation(account, bookmark.getFullJid(), true, true, false); - bookmark.setConversation(conversation); - } + public void fetchBookmarks2(final Account account) { + final IqPacket retrieve = mIqGenerator.retrieveBookmarks(); + sendIqPacket(account, retrieve, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, final IqPacket response) { + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); + final Map bookmarks = Bookmark.parseFromPubsub(pubsub, account); + processBookmarksInitial(account, bookmarks, true); } } - if (pep && synchronizeWithBookmarks) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + previousBookmarks.size() + " bookmarks have been removed"); - for (Jid jid : previousBookmarks) { - final Conversation conversation = find(account, jid); - if (conversation != null && conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": archiving destroyed conference ("+conversation.getJid()+") after receiving pep"); - archiveConversation(conversation, false); - } - } - } - } - account.setBookmarks(new CopyOnWriteArrayList<>(bookmarks.values())); + }); } - public void pushBookmarks(Account account) { + public void processBookmarksInitial(Account account, Map bookmarks, final boolean pep) { + final Set previousBookmarks = account.getBookmarkedJids(); + final boolean synchronizeWithBookmarks = synchronizeWithBookmarks(); + for (Bookmark bookmark : bookmarks.values()) { + previousBookmarks.remove(bookmark.getJid().asBareJid()); + processModifiedBookmark(bookmark, pep, synchronizeWithBookmarks); + } + if (pep && synchronizeWithBookmarks) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + previousBookmarks.size() + " bookmarks have been removed"); + for (Jid jid : previousBookmarks) { + processDeletedBookmark(account, jid); + } + } + account.setBookmarks(bookmarks); + } + + public void processDeletedBookmark(Account account, Jid jid) { + final Conversation conversation = find(account, jid); + if (conversation != null && conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving destroyed conference (" + conversation.getJid() + ") after receiving pep"); + archiveConversation(conversation, false); + } + } + + private void processModifiedBookmark(Bookmark bookmark, final boolean pep, final boolean synchronizeWithBookmarks) { + final Account account = bookmark.getAccount(); + Conversation conversation = find(bookmark); + if (conversation != null) { + if (conversation.getMode() != Conversation.MODE_MULTI) { + return; + } + bookmark.setConversation(conversation); + if (pep && synchronizeWithBookmarks && !bookmark.autojoin()) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conference (" + conversation.getJid() + ") after receiving pep"); + archiveConversation(conversation, false); + } else { + final MucOptions mucOptions = conversation.getMucOptions(); + if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) { + final String current = mucOptions.getActualNick(); + final String proposed = mucOptions.getProposedNick(); + if (current != null && !current.equals(proposed)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": proposed nick changed after bookmark push " + current + "->" + proposed); + joinMuc(conversation); + } + } + } + } else if (synchronizeWithBookmarks && bookmark.autojoin()) { + conversation = findOrCreateConversation(account, bookmark.getFullJid(), true, true, false); + bookmark.setConversation(conversation); + } + } + + public void processModifiedBookmark(Bookmark bookmark) { + final boolean synchronizeWithBookmarks = synchronizeWithBookmarks(); + processModifiedBookmark(bookmark, true, synchronizeWithBookmarks); + } + + public void createBookmark(final Account account, final Bookmark bookmark) { + account.putBookmark(bookmark); final XmppConnection connection = account.getXmppConnection(); - if (connection != null && connection.getFeatures().bookmarksConversion()) { + if (connection.getFeatures().bookmarks2()) { + final Element item = mIqGenerator.publishBookmarkItem(bookmark); + pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS2, item, bookmark.getJid().asBareJid().toEscapedString(), PublishOptions.persistentWhitelistAccessMaxItems()); + } else if (connection.getFeatures().bookmarksConversion()) { + pushBookmarksPep(account); + } else { + pushBookmarksPrivateXml(account); + } + } + + public void deleteBookmark(final Account account, final Bookmark bookmark) { + account.removeBookmark(bookmark); + final XmppConnection connection = account.getXmppConnection(); + if (connection.getFeatures().bookmarks2()) { + IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString()); + sendIqPacket(account, request, (a, response) -> { + if (response.getType() == IqPacket.TYPE.ERROR) { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getError()); + } + }); + } else if (connection.getFeatures().bookmarksConversion()) { pushBookmarksPep(account); } else { pushBookmarksPrivateXml(account); @@ -1639,18 +1690,22 @@ public class XmppConnectionService extends Service { for (Bookmark bookmark : account.getBookmarks()) { storage.addChild(bookmark); } - pushNodeAndEnforcePublishOptions(account,Namespace.BOOKMARKS,storage, PublishOptions.persistentWhitelistAccess()); + pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, PublishOptions.persistentWhitelistAccess()); } - private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final Bundle options) { - pushNodeAndEnforcePublishOptions(account, node, element, options, true); + pushNodeAndEnforcePublishOptions(account, node, element, null, options, true); } - private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final Bundle options, final boolean retry) { - final IqPacket packet = mIqGenerator.publishElement(node, element, options); + private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options) { + pushNodeAndEnforcePublishOptions(account, node, element, id, options, true); + + } + + private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options, final boolean retry) { + final IqPacket packet = mIqGenerator.publishElement(node, element, id, options); sendIqPacket(account, packet, (a, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { return; @@ -1659,86 +1714,86 @@ public class XmppConnectionService extends Service { pushNodeConfiguration(account, node, options, new OnConfigurationPushed() { @Override public void onPushSucceeded() { - pushNodeAndEnforcePublishOptions(account, node, element, options, false); + pushNodeAndEnforcePublishOptions(account, node, element, id, options, false); } @Override public void onPushFailed() { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to push node configuration ("+node+")"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to push node configuration (" + node + ")"); } }); } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": error publishing bookmarks (retry="+Boolean.toString(retry)+") "+response); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing bookmarks (retry=" + Boolean.toString(retry) + ") " + response); } }); } - private void restoreFromDatabase() { - synchronized (this.conversations) { - final Map accountLookupTable = new Hashtable<>(); - for (Account account : this.accounts) { - accountLookupTable.put(account.getUuid(), account); - } - Log.d(Config.LOGTAG, "restoring conversations..."); - final long startTimeConversationsRestore = SystemClock.elapsedRealtime(); - this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE)); - for (Iterator iterator = conversations.listIterator(); iterator.hasNext(); ) { - Conversation conversation = iterator.next(); - Account account = accountLookupTable.get(conversation.getAccountUuid()); - if (account != null) { - conversation.setAccount(account); - } else { - Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid()); - iterator.remove(); - } - } - long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore; - Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms"); - Runnable runnable = () -> { - long deletionDate = getAutomaticMessageDeletionDate(); - mLastExpiryRun.set(SystemClock.elapsedRealtime()); - if (deletionDate > 0) { - Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate)); - databaseBackend.expireOldMessages(deletionDate); - } - Log.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 - } - getBitmapCache().evictAll(); - loadPhoneContacts(); - Log.d(Config.LOGTAG, "restoring messages..."); - final long startMessageRestore = SystemClock.elapsedRealtime(); - final Conversation quickLoad = QuickLoader.get(this.conversations); - if (quickLoad != null) { - restoreMessages(quickLoad); - updateConversationUi(); - final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore; - Log.d(Config.LOGTAG,"quickly restored "+quickLoad.getName()+" after " + diffMessageRestore + "ms"); - } - for (Conversation conversation : this.conversations) { - if (quickLoad != conversation) { - restoreMessages(conversation); - } - } - mNotificationService.finishBacklog(false); - restoredFromDatabaseLatch.countDown(); - final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore; - Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms"); - updateConversationUi(); - }; - mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine - } - } + private void restoreFromDatabase() { + synchronized (this.conversations) { + final Map accountLookupTable = new Hashtable<>(); + for (Account account : this.accounts) { + accountLookupTable.put(account.getUuid(), account); + } + Log.d(Config.LOGTAG, "restoring conversations..."); + final long startTimeConversationsRestore = SystemClock.elapsedRealtime(); + this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE)); + for (Iterator iterator = conversations.listIterator(); iterator.hasNext(); ) { + Conversation conversation = iterator.next(); + Account account = accountLookupTable.get(conversation.getAccountUuid()); + if (account != null) { + conversation.setAccount(account); + } else { + Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid()); + iterator.remove(); + } + } + long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore; + Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms"); + Runnable runnable = () -> { + long deletionDate = getAutomaticMessageDeletionDate(); + mLastExpiryRun.set(SystemClock.elapsedRealtime()); + if (deletionDate > 0) { + Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate)); + databaseBackend.expireOldMessages(deletionDate); + } + Log.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 + } + getBitmapCache().evictAll(); + loadPhoneContacts(); + Log.d(Config.LOGTAG, "restoring messages..."); + final long startMessageRestore = SystemClock.elapsedRealtime(); + final Conversation quickLoad = QuickLoader.get(this.conversations); + if (quickLoad != null) { + restoreMessages(quickLoad); + updateConversationUi(); + final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore; + Log.d(Config.LOGTAG, "quickly restored " + quickLoad.getName() + " after " + diffMessageRestore + "ms"); + } + for (Conversation conversation : this.conversations) { + if (quickLoad != conversation) { + restoreMessages(conversation); + } + } + mNotificationService.finishBacklog(false); + restoredFromDatabaseLatch.countDown(); + final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore; + Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms"); + updateConversationUi(); + }; + mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine + } + } - private void restoreMessages(Conversation conversation) { - conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); - conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); - conversation.findUnreadMessages(message -> mNotificationService.pushFromBacklog(message)); - } + private void restoreMessages(Conversation conversation) { + conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); + conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); + conversation.findUnreadMessages(message -> mNotificationService.pushFromBacklog(message)); + } - public void loadPhoneContacts() { + public void loadPhoneContacts() { mContactMergerExecutor.execute(() -> { Map contacts = JabberIdContact.load(this); Log.d(Config.LOGTAG, "start merging phone contacts with roster"); @@ -1764,26 +1819,26 @@ public class XmppConnectionService extends Service { updateRosterUi(); mQuickConversationsService.considerSync(); }); - } + } - public void syncRoster(final Account account) { - mRosterSyncTaskManager.execute(account, () -> databaseBackend.writeRoster(account.getRoster())); - } + public void syncRoster(final Account account) { + mRosterSyncTaskManager.execute(account, () -> databaseBackend.writeRoster(account.getRoster())); + } - public List getConversations() { - return this.conversations; - } + public List getConversations() { + return this.conversations; + } - private void markFileDeleted(final String path) { + private void markFileDeleted(final String path) { final File file = new File(path); final boolean isInternalFile = fileBackend.isInternalFile(file); final List uuids = databaseBackend.markFileAsDeleted(file, isInternalFile); - Log.d(Config.LOGTAG, "deleted file " + path+" internal="+isInternalFile+", database hits="+uuids.size()); + Log.d(Config.LOGTAG, "deleted file " + path + " internal=" + isInternalFile + ", database hits=" + uuids.size()); markUuidsAsDeletedFiles(uuids); - } + } - private void markUuidsAsDeletedFiles(List uuids) { + private void markUuidsAsDeletedFiles(List uuids) { boolean deleted = false; for (Conversation conversation : getConversations()) { deleted |= conversation.markAsDeleted(uuids); @@ -1803,37 +1858,37 @@ public class XmppConnectionService extends Service { } } - public void populateWithOrderedConversations(final List list) { - populateWithOrderedConversations(list, true, true); - } + public void populateWithOrderedConversations(final List list) { + populateWithOrderedConversations(list, true, true); + } - public void populateWithOrderedConversations(final List list, final boolean includeNoFileUpload) { + public void populateWithOrderedConversations(final List list, final boolean includeNoFileUpload) { populateWithOrderedConversations(list, includeNoFileUpload, true); } - public void populateWithOrderedConversations(final List list, final boolean includeNoFileUpload, final boolean sort) { + public void populateWithOrderedConversations(final List list, final boolean includeNoFileUpload, final boolean sort) { final List orderedUuids; if (sort) { orderedUuids = null; } else { orderedUuids = new ArrayList<>(); - for(Conversation conversation : list) { + for (Conversation conversation : list) { orderedUuids.add(conversation.getUuid()); } } - list.clear(); - if (includeNoFileUpload) { - list.addAll(getConversations()); - } else { - for (Conversation conversation : getConversations()) { - if (conversation.getMode() == Conversation.MODE_SINGLE - || (conversation.getAccount().httpUploadAvailable() && conversation.getMucOptions().participating())) { - list.add(conversation); - } - } - } - try { - if (orderedUuids != null) { + list.clear(); + if (includeNoFileUpload) { + list.addAll(getConversations()); + } else { + for (Conversation conversation : getConversations()) { + if (conversation.getMode() == Conversation.MODE_SINGLE + || (conversation.getAccount().httpUploadAvailable() && conversation.getMucOptions().participating())) { + list.add(conversation); + } + } + } + try { + if (orderedUuids != null) { Collections.sort(list, (a, b) -> { final int indexA = orderedUuids.indexOf(a.getUuid()); final int indexB = orderedUuids.indexOf(b.getUuid()); @@ -1845,836 +1900,850 @@ public class XmppConnectionService extends Service { } else { Collections.sort(list); } - } catch (IllegalArgumentException e) { - //ignore - } - } + } catch (IllegalArgumentException e) { + //ignore + } + } - public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { - if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) { - return; - } else if (timestamp == 0) { - return; - } - Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); - final Runnable runnable = () -> { - final Account account = conversation.getAccount(); - List messages = databaseBackend.getMessages(conversation, 50, timestamp); - if (messages.size() > 0) { - conversation.addAll(0, messages); - callback.onMoreMessagesLoaded(messages.size(), conversation); - } else if (conversation.hasMessagesLeftOnServer() - && account.isOnlineAndConnected() - && conversation.getLastClearHistory().getTimestamp() == 0) { - final boolean mamAvailable; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - mamAvailable = account.getXmppConnection().getFeatures().mam() && !conversation.getContact().isBlocked(); - } else { - mamAvailable = conversation.getMucOptions().mamSupport(); - } - if (mamAvailable) { - MessageArchiveService.Query query = getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false); - if (query != null) { - query.setCallback(callback); - callback.informUser(R.string.fetching_history_from_server); - } else { - callback.informUser(R.string.not_fetching_history_retention_period); - } + public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { + if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) { + return; + } else if (timestamp == 0) { + return; + } + Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); + final Runnable runnable = () -> { + final Account account = conversation.getAccount(); + List messages = databaseBackend.getMessages(conversation, 50, timestamp); + if (messages.size() > 0) { + conversation.addAll(0, messages); + callback.onMoreMessagesLoaded(messages.size(), conversation); + } else if (conversation.hasMessagesLeftOnServer() + && account.isOnlineAndConnected() + && conversation.getLastClearHistory().getTimestamp() == 0) { + final boolean mamAvailable; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + mamAvailable = account.getXmppConnection().getFeatures().mam() && !conversation.getContact().isBlocked(); + } else { + mamAvailable = conversation.getMucOptions().mamSupport(); + } + if (mamAvailable) { + MessageArchiveService.Query query = getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false); + if (query != null) { + query.setCallback(callback); + callback.informUser(R.string.fetching_history_from_server); + } else { + callback.informUser(R.string.not_fetching_history_retention_period); + } - } - } - }; - mDatabaseReaderExecutor.execute(runnable); - } + } + } + }; + mDatabaseReaderExecutor.execute(runnable); + } - public List getAccounts() { - return this.accounts; - } + public List getAccounts() { + return this.accounts; + } /** * This will find all conferences with the contact as member and also the conference that is the contact (that 'fake' contact is used to store the avatar) */ - public List findAllConferencesWith(Contact contact) { - ArrayList results = new ArrayList<>(); - for (final Conversation c : conversations) { - if (c.getMode() == Conversation.MODE_MULTI && (c.getJid().asBareJid().equals(contact.getJid().asBareJid()) || c.getMucOptions().isContactInRoom(contact))) { - results.add(c); - } - } - return results; - } - - public Conversation find(final Iterable haystack, final Contact contact) { - for (final Conversation conversation : haystack) { - if (conversation.getContact() == contact) { - return conversation; - } - } - return null; - } - - public Conversation find(final Iterable haystack, final Account account, final Jid jid) { - if (jid == null) { - return null; - } - for (final Conversation conversation : haystack) { - if ((account == null || conversation.getAccount() == account) - && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) { - return conversation; - } - } - return null; - } - - public boolean isConversationsListEmpty(final Conversation ignore) { - synchronized (this.conversations) { - final int size = this.conversations.size(); - return size == 0 || size == 1 && this.conversations.get(0) == ignore; - } - } - - public boolean isConversationStillOpen(final Conversation conversation) { - synchronized (this.conversations) { - for (Conversation current : this.conversations) { - if (current == conversation) { - return true; - } - } - } - return false; - } - - public Conversation findOrCreateConversation(Account account, Jid jid, boolean muc, final boolean async) { - return this.findOrCreateConversation(account, jid, muc, false, async); - } - - public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final boolean async) { - return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async); - } - - public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) { - synchronized (this.conversations) { - Conversation conversation = find(account, jid); - if (conversation != null) { - return conversation; - } - conversation = databaseBackend.findConversation(account, jid); - final boolean loadMessagesFromDb; - if (conversation != null) { - conversation.setStatus(Conversation.STATUS_AVAILABLE); - conversation.setAccount(account); - if (muc) { - conversation.setMode(Conversation.MODE_MULTI); - conversation.setContactJid(jid); - } else { - conversation.setMode(Conversation.MODE_SINGLE); - conversation.setContactJid(jid.asBareJid()); - } - databaseBackend.updateConversation(conversation); - loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true, false); - } else { - String conversationName; - Contact contact = account.getRoster().getContact(jid); - if (contact != null) { - conversationName = contact.getDisplayName(); - } else { - conversationName = jid.getLocal(); - } - if (muc) { - conversation = new Conversation(conversationName, account, jid, - Conversation.MODE_MULTI); - } else { - conversation = new Conversation(conversationName, account, jid.asBareJid(), - Conversation.MODE_SINGLE); - } - this.databaseBackend.createConversation(conversation); - loadMessagesFromDb = false; - } - final Conversation c = conversation; - final Runnable runnable = () -> { - if (loadMessagesFromDb) { - c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE)); - updateConversationUi(); - c.messagesLoaded.set(true); - } - if (account.getXmppConnection() != null - && !c.getContact().isBlocked() - && account.getXmppConnection().getFeatures().mam() - && !muc) { - if (query == null) { - mMessageArchiveService.query(c); - } else { - if (query.getConversation() == null) { - mMessageArchiveService.query(c, query.getStart(), query.isCatchup()); - } - } - } - if (joinAfterCreate) { - joinMuc(c); - } - }; - if (async) { - mDatabaseReaderExecutor.execute(runnable); - } else { - runnable.run(); - } - this.conversations.add(conversation); - updateConversationUi(); - return conversation; - } - } - - public void archiveConversation(Conversation conversation) { - archiveConversation(conversation, true); + public List findAllConferencesWith(Contact contact) { + ArrayList results = new ArrayList<>(); + for (final Conversation c : conversations) { + if (c.getMode() == Conversation.MODE_MULTI && (c.getJid().asBareJid().equals(contact.getJid().asBareJid()) || c.getMucOptions().isContactInRoom(contact))) { + results.add(c); + } + } + return results; } - private void archiveConversation(Conversation conversation, final boolean maySynchronizeWithBookmarks) { - getNotificationService().clear(conversation); - conversation.setStatus(Conversation.STATUS_ARCHIVED); - conversation.setNextMessage(null); - synchronized (this.conversations) { - getMessageArchiveService().kill(conversation); - if (conversation.getMode() == Conversation.MODE_MULTI) { - if (conversation.getAccount().getStatus() == Account.State.ONLINE) { - Bookmark bookmark = conversation.getBookmark(); - if (maySynchronizeWithBookmarks && bookmark != null && synchronizeWithBookmarks()) { - if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) { - Account account = bookmark.getAccount(); - bookmark.setConversation(null); - account.getBookmarks().remove(bookmark); - pushBookmarks(account); - } else if (bookmark.autojoin()) { - bookmark.setAutojoin(false); - pushBookmarks(bookmark.getAccount()); - } - } - } + public Conversation find(final Iterable haystack, final Contact contact) { + for (final Conversation conversation : haystack) { + if (conversation.getContact() == contact) { + return conversation; + } + } + return null; + } + + public Conversation find(final Iterable haystack, final Account account, final Jid jid) { + if (jid == null) { + return null; + } + for (final Conversation conversation : haystack) { + if ((account == null || conversation.getAccount() == account) + && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) { + return conversation; + } + } + return null; + } + + public boolean isConversationsListEmpty(final Conversation ignore) { + synchronized (this.conversations) { + final int size = this.conversations.size(); + return size == 0 || size == 1 && this.conversations.get(0) == ignore; + } + } + + public boolean isConversationStillOpen(final Conversation conversation) { + synchronized (this.conversations) { + for (Conversation current : this.conversations) { + if (current == conversation) { + return true; + } + } + } + return false; + } + + public Conversation findOrCreateConversation(Account account, Jid jid, boolean muc, final boolean async) { + return this.findOrCreateConversation(account, jid, muc, false, async); + } + + public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final boolean async) { + return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async); + } + + public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) { + synchronized (this.conversations) { + Conversation conversation = find(account, jid); + if (conversation != null) { + return conversation; + } + conversation = databaseBackend.findConversation(account, jid); + final boolean loadMessagesFromDb; + if (conversation != null) { + conversation.setStatus(Conversation.STATUS_AVAILABLE); + conversation.setAccount(account); + if (muc) { + conversation.setMode(Conversation.MODE_MULTI); + conversation.setContactJid(jid); + } else { + conversation.setMode(Conversation.MODE_SINGLE); + conversation.setContactJid(jid.asBareJid()); + } + databaseBackend.updateConversation(conversation); + loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true, false); + } else { + String conversationName; + Contact contact = account.getRoster().getContact(jid); + if (contact != null) { + conversationName = contact.getDisplayName(); + } else { + conversationName = jid.getLocal(); + } + if (muc) { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_MULTI); + } else { + conversation = new Conversation(conversationName, account, jid.asBareJid(), + Conversation.MODE_SINGLE); + } + this.databaseBackend.createConversation(conversation); + loadMessagesFromDb = false; + } + final Conversation c = conversation; + final Runnable runnable = () -> { + if (loadMessagesFromDb) { + c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE)); + updateConversationUi(); + c.messagesLoaded.set(true); + } + if (account.getXmppConnection() != null + && !c.getContact().isBlocked() + && account.getXmppConnection().getFeatures().mam() + && !muc) { + if (query == null) { + mMessageArchiveService.query(c); + } else { + if (query.getConversation() == null) { + mMessageArchiveService.query(c, query.getStart(), query.isCatchup()); + } + } + } + if (joinAfterCreate) { + joinMuc(c); + } + }; + if (async) { + mDatabaseReaderExecutor.execute(runnable); + } else { + runnable.run(); + } + this.conversations.add(conversation); + updateConversationUi(); + return conversation; + } + } + + public void archiveConversation(Conversation conversation) { + archiveConversation(conversation, true); + } + + private void archiveConversation(Conversation conversation, final boolean maySynchronizeWithBookmarks) { + getNotificationService().clear(conversation); + conversation.setStatus(Conversation.STATUS_ARCHIVED); + conversation.setNextMessage(null); + synchronized (this.conversations) { + getMessageArchiveService().kill(conversation); + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getAccount().getStatus() == Account.State.ONLINE) { + final Bookmark bookmark = conversation.getBookmark(); + if (maySynchronizeWithBookmarks && bookmark != null && synchronizeWithBookmarks()) { + if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) { + Account account = bookmark.getAccount(); + bookmark.setConversation(null); + deleteBookmark(account, bookmark); + } else if (bookmark.autojoin()) { + bookmark.setAutojoin(false); + createBookmark(bookmark.getAccount(), bookmark); + } + } + } if (conversation.getMucOptions().push()) { disableDirectMucPush(conversation); mPushManagementService.disablePushOnServer(conversation); } - leaveMuc(conversation); - } else { - if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - stopPresenceUpdatesTo(conversation.getContact()); - } - } - updateConversation(conversation); - this.conversations.remove(conversation); - updateConversationUi(); - } - } + leaveMuc(conversation); + } else { + if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + stopPresenceUpdatesTo(conversation.getContact()); + } + } + updateConversation(conversation); + this.conversations.remove(conversation); + updateConversationUi(); + } + } - public void stopPresenceUpdatesTo(Contact contact) { + public void stopPresenceUpdatesTo(Contact contact) { Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString()); sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact)); contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); } - public void createAccount(final Account account) { - account.initAccountServices(this); - databaseBackend.createAccount(account); - this.accounts.add(account); - this.reconnectAccountInBackground(account); - updateAccountUi(); - syncEnabledAccountSetting(); - toggleForegroundService(); - } + public void createAccount(final Account account) { + account.initAccountServices(this); + databaseBackend.createAccount(account); + this.accounts.add(account); + this.reconnectAccountInBackground(account); + updateAccountUi(); + syncEnabledAccountSetting(); + toggleForegroundService(); + } - private void syncEnabledAccountSetting() { - final boolean hasEnabledAccounts = hasEnabledAccounts(); - getPreferences().edit().putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); - toggleSetProfilePictureActivity(hasEnabledAccounts); - } + private void syncEnabledAccountSetting() { + final boolean hasEnabledAccounts = hasEnabledAccounts(); + getPreferences().edit().putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); + toggleSetProfilePictureActivity(hasEnabledAccounts); + } - private void toggleSetProfilePictureActivity(final boolean enabled) { - try { - final ComponentName name = new ComponentName(this, ChooseAccountForProfilePictureActivity.class); - final int targetState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + private void toggleSetProfilePictureActivity(final boolean enabled) { + try { + final ComponentName name = new ComponentName(this, ChooseAccountForProfilePictureActivity.class); + final int targetState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; getPackageManager().setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP); } catch (IllegalStateException e) { - Log.d(Config.LOGTAG,"unable to toggle profile picture actvitiy"); + Log.d(Config.LOGTAG, "unable to toggle profile picture actvitiy"); } } - public void createAccountFromKey(final String alias, final OnAccountCreated callback) { - new Thread(() -> { - try { - final X509Certificate[] chain = KeyChain.getCertificateChain(this, alias); - final X509Certificate cert = chain != null && chain.length > 0 ? chain[0] : null; - if (cert == null) { - callback.informUser(R.string.unable_to_parse_certificate); - return; - } - Pair info = CryptoHelper.extractJidAndName(cert); - if (info == null) { - callback.informUser(R.string.certificate_does_not_contain_jid); - return; - } - 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(account.getJid().getDomain()).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 createAccountFromKey(final String alias, final OnAccountCreated callback) { + new Thread(() -> { + try { + final X509Certificate[] chain = KeyChain.getCertificateChain(this, alias); + final X509Certificate cert = chain != null && chain.length > 0 ? chain[0] : null; + if (cert == null) { + callback.informUser(R.string.unable_to_parse_certificate); + return; + } + Pair info = CryptoHelper.extractJidAndName(cert); + if (info == null) { + callback.informUser(R.string.certificate_does_not_contain_jid); + return; + } + 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(account.getJid().getDomain()).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, account.getJid().asBareJid() + ": update key in account " + alias); - try { - X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain"); - Pair info = CryptoHelper.extractJidAndName(chain[0]); - if (info == null) { - showErrorToastInUi(R.string.certificate_does_not_contain_jid); - return; - } - if (account.getJid().asBareJid().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 updateKeyInAccount(final Account account, final String alias) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias); + try { + X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain"); + Pair info = CryptoHelper.extractJidAndName(chain[0]); + if (info == null) { + showErrorToastInUi(R.string.certificate_does_not_contain_jid); + return; + } + if (account.getJid().asBareJid().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 boolean updateAccount(final Account account) { - if (databaseBackend.updateAccount(account)) { - account.setShowErrorNotification(true); - this.statusListener.onStatusChanged(account); - databaseBackend.updateAccount(account); - reconnectAccountInBackground(account); - updateAccountUi(); - getNotificationService().updateErrorNotification(); - toggleForegroundService(); - syncEnabledAccountSetting(); - return true; - } else { - return false; - } - } + public boolean updateAccount(final Account account) { + if (databaseBackend.updateAccount(account)) { + account.setShowErrorNotification(true); + this.statusListener.onStatusChanged(account); + databaseBackend.updateAccount(account); + reconnectAccountInBackground(account); + updateAccountUi(); + getNotificationService().updateErrorNotification(); + toggleForegroundService(); + syncEnabledAccountSetting(); + return true; + } else { + return false; + } + } - public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) { - final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword); - sendIqPacket(account, iq, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - a.setPassword(newPassword); - a.setOption(Account.OPTION_MAGIC_CREATE, false); - databaseBackend.updateAccount(a); - callback.onPasswordChangeSucceeded(); - } else { - callback.onPasswordChangeFailed(); - } - }); - } + public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) { + final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword); + sendIqPacket(account, iq, (a, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + a.setPassword(newPassword); + a.setOption(Account.OPTION_MAGIC_CREATE, false); + databaseBackend.updateAccount(a); + callback.onPasswordChangeSucceeded(); + } else { + callback.onPasswordChangeFailed(); + } + }); + } - public void deleteAccount(final Account account) { - synchronized (this.conversations) { - for (final Conversation conversation : conversations) { - if (conversation.getAccount() == account) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - leaveMuc(conversation); - } - conversations.remove(conversation); - mNotificationService.clear(conversation); - } - } - if (account.getXmppConnection() != null) { - new Thread(() -> disconnect(account, true)).start(); - } - final Runnable runnable = () -> { - if (!databaseBackend.deleteAccount(account)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to delete account"); - } - }; - mDatabaseWriterExecutor.execute(runnable); - this.accounts.remove(account); - this.mRosterSyncTaskManager.clear(account); - updateAccountUi(); - mNotificationService.updateErrorNotification(); - syncEnabledAccountSetting(); - toggleForegroundService(); - } - } + public void deleteAccount(final Account account) { + final boolean connected = account.getStatus() == Account.State.ONLINE; + synchronized (this.conversations) { + if (connected) { + account.getAxolotlService().deleteOmemoIdentity(); + } + for (final Conversation conversation : conversations) { + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (connected) { + leaveMuc(conversation); + } + } + conversations.remove(conversation); + mNotificationService.clear(conversation); + } + } + if (account.getXmppConnection() != null) { + new Thread(() -> disconnect(account, !connected)).start(); + } + final Runnable runnable = () -> { + if (!databaseBackend.deleteAccount(account)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to delete account"); + } + }; + mDatabaseWriterExecutor.execute(runnable); + this.accounts.remove(account); + this.mRosterSyncTaskManager.clear(account); + updateAccountUi(); + mNotificationService.updateErrorNotification(); + syncEnabledAccountSetting(); + toggleForegroundService(); + } + } - public void setOnConversationListChangedListener(OnConversationUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnConversationUpdates.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as ConversationListChangedListener"); - } - this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0); - } - if (remainingListeners) { - switchToForeground(); - } - } + public void setOnConversationListChangedListener(OnConversationUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + remainingListeners = checkListeners(); + if (!this.mOnConversationUpdates.add(listener)) { + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as ConversationListChangedListener"); + } + this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0); + } + if (remainingListeners) { + switchToForeground(); + } + } - public void removeOnConversationListChangedListener(OnConversationUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnConversationUpdates.remove(listener); - this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } + public void removeOnConversationListChangedListener(OnConversationUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + this.mOnConversationUpdates.remove(listener); + this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0); + remainingListeners = checkListeners(); + } + if (remainingListeners) { + switchToBackground(); + } + } - public void setOnShowErrorToastListener(OnShowErrorToast listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnShowErrorToasts.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnShowErrorToastListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } + public void setOnShowErrorToastListener(OnShowErrorToast listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + remainingListeners = checkListeners(); + if (!this.mOnShowErrorToasts.add(listener)) { + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnShowErrorToastListener"); + } + } + if (remainingListeners) { + switchToForeground(); + } + } - public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnShowErrorToasts.remove(onShowErrorToast); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } + public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + this.mOnShowErrorToasts.remove(onShowErrorToast); + remainingListeners = checkListeners(); + } + if (remainingListeners) { + switchToBackground(); + } + } - public void setOnAccountListChangedListener(OnAccountUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnAccountUpdates.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnAccountListChangedtListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } + public void setOnAccountListChangedListener(OnAccountUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + remainingListeners = checkListeners(); + if (!this.mOnAccountUpdates.add(listener)) { + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnAccountListChangedtListener"); + } + } + if (remainingListeners) { + switchToForeground(); + } + } - public void removeOnAccountListChangedListener(OnAccountUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnAccountUpdates.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } + public void removeOnAccountListChangedListener(OnAccountUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + this.mOnAccountUpdates.remove(listener); + remainingListeners = checkListeners(); + } + if (remainingListeners) { + switchToBackground(); + } + } - public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnCaptchaRequested.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnCaptchaRequestListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } + public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + remainingListeners = checkListeners(); + if (!this.mOnCaptchaRequested.add(listener)) { + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnCaptchaRequestListener"); + } + } + if (remainingListeners) { + switchToForeground(); + } + } - public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnCaptchaRequested.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } + public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + this.mOnCaptchaRequested.remove(listener); + remainingListeners = checkListeners(); + } + if (remainingListeners) { + switchToBackground(); + } + } - public void setOnRosterUpdateListener(final OnRosterUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnRosterUpdates.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnRosterUpdateListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } + public void setOnRosterUpdateListener(final OnRosterUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + remainingListeners = checkListeners(); + if (!this.mOnRosterUpdates.add(listener)) { + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnRosterUpdateListener"); + } + } + if (remainingListeners) { + switchToForeground(); + } + } - public void removeOnRosterUpdateListener(final OnRosterUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnRosterUpdates.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } + public void removeOnRosterUpdateListener(final OnRosterUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + this.mOnRosterUpdates.remove(listener); + remainingListeners = checkListeners(); + } + if (remainingListeners) { + switchToBackground(); + } + } - public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnUpdateBlocklist.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnUpdateBlocklistListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } + public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + remainingListeners = checkListeners(); + if (!this.mOnUpdateBlocklist.add(listener)) { + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnUpdateBlocklistListener"); + } + } + if (remainingListeners) { + switchToForeground(); + } + } - public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnUpdateBlocklist.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } + public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + this.mOnUpdateBlocklist.remove(listener); + remainingListeners = checkListeners(); + } + if (remainingListeners) { + switchToBackground(); + } + } - public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnKeyStatusUpdated.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnKeyStatusUpdateListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } + public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + remainingListeners = checkListeners(); + if (!this.mOnKeyStatusUpdated.add(listener)) { + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnKeyStatusUpdateListener"); + } + } + if (remainingListeners) { + switchToForeground(); + } + } - public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnKeyStatusUpdated.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } + public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + this.mOnKeyStatusUpdated.remove(listener); + remainingListeners = checkListeners(); + } + if (remainingListeners) { + switchToBackground(); + } + } - public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnMucRosterUpdate.add(listener)) { - Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnMucRosterListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } + public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + remainingListeners = checkListeners(); + if (!this.mOnMucRosterUpdate.add(listener)) { + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnMucRosterListener"); + } + } + if (remainingListeners) { + switchToForeground(); + } + } - public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnMucRosterUpdate.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } + public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + this.mOnMucRosterUpdate.remove(listener); + remainingListeners = checkListeners(); + } + if (remainingListeners) { + switchToBackground(); + } + } - public boolean checkListeners() { - return (this.mOnAccountUpdates.size() == 0 - && this.mOnConversationUpdates.size() == 0 - && this.mOnRosterUpdates.size() == 0 - && this.mOnCaptchaRequested.size() == 0 - && this.mOnMucRosterUpdate.size() == 0 - && this.mOnUpdateBlocklist.size() == 0 - && this.mOnShowErrorToasts.size() == 0 - && this.mOnKeyStatusUpdated.size() == 0); - } + public boolean checkListeners() { + return (this.mOnAccountUpdates.size() == 0 + && this.mOnConversationUpdates.size() == 0 + && this.mOnRosterUpdates.size() == 0 + && this.mOnCaptchaRequested.size() == 0 + && this.mOnMucRosterUpdate.size() == 0 + && this.mOnUpdateBlocklist.size() == 0 + && this.mOnShowErrorToasts.size() == 0 + && this.mOnKeyStatusUpdated.size() == 0); + } - private void switchToForeground() { - final boolean broadcastLastActivity = broadcastLastActivity(); - for (Conversation conversation : getConversations()) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - conversation.getMucOptions().resetChatState(); - } else { - conversation.setIncomingChatState(Config.DEFAULT_CHATSTATE); - } - } - for (Account account : getAccounts()) { - if (account.getStatus() == Account.State.ONLINE) { - account.deactivateGracePeriod(); - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - if (connection.getFeatures().csi()) { - connection.sendActive(); - } - if (broadcastLastActivity) { - sendPresence(account, false); //send new presence but don't include idle because we are not - } - } - } - } - Log.d(Config.LOGTAG, "app switched into foreground"); - } + private void switchToForeground() { + final boolean broadcastLastActivity = broadcastLastActivity(); + for (Conversation conversation : getConversations()) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + conversation.getMucOptions().resetChatState(); + } else { + conversation.setIncomingChatState(Config.DEFAULT_CHATSTATE); + } + } + for (Account account : getAccounts()) { + if (account.getStatus() == Account.State.ONLINE) { + account.deactivateGracePeriod(); + final XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + if (connection.getFeatures().csi()) { + connection.sendActive(); + } + if (broadcastLastActivity) { + sendPresence(account, false); //send new presence but don't include idle because we are not + } + } + } + } + Log.d(Config.LOGTAG, "app switched into foreground"); + } - private void switchToBackground() { - final boolean broadcastLastActivity = broadcastLastActivity(); - if (broadcastLastActivity) { - mLastActivity = System.currentTimeMillis(); - final SharedPreferences.Editor editor = getPreferences().edit(); - editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity); - editor.apply(); - } - for (Account account : getAccounts()) { - if (account.getStatus() == Account.State.ONLINE) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - if (broadcastLastActivity) { - sendPresence(account, true); - } - if (connection.getFeatures().csi()) { - connection.sendInactive(); - } - } - } - } - this.mNotificationService.setIsInForeground(false); - Log.d(Config.LOGTAG, "app switched into background"); - } + private void switchToBackground() { + final boolean broadcastLastActivity = broadcastLastActivity(); + if (broadcastLastActivity) { + mLastActivity = System.currentTimeMillis(); + final SharedPreferences.Editor editor = getPreferences().edit(); + editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity); + editor.apply(); + } + for (Account account : getAccounts()) { + if (account.getStatus() == Account.State.ONLINE) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + if (broadcastLastActivity) { + sendPresence(account, true); + } + if (connection.getFeatures().csi()) { + connection.sendInactive(); + } + } + } + } + this.mNotificationService.setIsInForeground(false); + Log.d(Config.LOGTAG, "app switched into background"); + } - private void connectMultiModeConversations(Account account) { - List conversations = getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) { - joinMuc(conversation); - } - } - } + private void connectMultiModeConversations(Account account) { + List conversations = getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) { + joinMuc(conversation); + } + } + } - public void mucSelfPingAndRejoin(final Conversation conversation) { - final Account account = conversation.getAccount(); - synchronized (account.inProgressConferenceJoins) { + public void mucSelfPingAndRejoin(final Conversation conversation) { + final Account account = conversation.getAccount(); + synchronized (account.inProgressConferenceJoins) { if (account.inProgressConferenceJoins.contains(conversation)) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": canceling muc self ping because join is already under way"); return; } } synchronized (account.inProgressConferencePings) { - if (!account.inProgressConferencePings.add(conversation)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid()+": canceling muc self ping because ping is already under way"); - return; + if (!account.inProgressConferencePings.add(conversation)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": canceling muc self ping because ping is already under way"); + return; } } - final Jid self = conversation.getMucOptions().getSelf().getFullJid(); - final IqPacket ping = new IqPacket(IqPacket.TYPE.GET); - ping.setTo(self); - ping.addChild("ping", Namespace.PING); - sendIqPacket(conversation.getAccount(), ping, (a, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Element error = response.findChild("error"); - if (error == null || error.hasChild("service-unavailable") || error.hasChild("feature-not-implemented") || error.hasChild("item-not-found")) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": ping to "+self+" came back as ignorable error"); + final Jid self = conversation.getMucOptions().getSelf().getFullJid(); + final IqPacket ping = new IqPacket(IqPacket.TYPE.GET); + ping.setTo(self); + ping.addChild("ping", Namespace.PING); + sendIqPacket(conversation.getAccount(), ping, (a, response) -> { + if (response.getType() == IqPacket.TYPE.ERROR) { + Element error = response.findChild("error"); + if (error == null || error.hasChild("service-unavailable") || error.hasChild("feature-not-implemented") || error.hasChild("item-not-found")) { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back as ignorable error"); } else { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": ping to "+self+" failed. attempting rejoin"); - joinMuc(conversation); + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " failed. attempting rejoin"); + joinMuc(conversation); } } else if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": ping to "+self+" came back fine"); + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back fine"); } - synchronized (account.inProgressConferencePings) { - account.inProgressConferencePings.remove(conversation); + synchronized (account.inProgressConferencePings) { + account.inProgressConferencePings.remove(conversation); } }); } - public void joinMuc(Conversation conversation) { - joinMuc(conversation, null, false); - } + public void joinMuc(Conversation conversation) { + joinMuc(conversation, null, false); + } - public void joinMuc(Conversation conversation, boolean followedInvite) { - joinMuc(conversation, null, followedInvite); - } + public void joinMuc(Conversation conversation, boolean followedInvite) { + joinMuc(conversation, null, followedInvite); + } - private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) { - joinMuc(conversation, onConferenceJoined, false); - } + private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) { + joinMuc(conversation, onConferenceJoined, false); + } - private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) { - final Account account = conversation.getAccount(); - synchronized (account.pendingConferenceJoins) { + private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) { + final Account account = conversation.getAccount(); + synchronized (account.pendingConferenceJoins) { account.pendingConferenceJoins.remove(conversation); } synchronized (account.pendingConferenceLeaves) { account.pendingConferenceLeaves.remove(conversation); } - if (account.getStatus() == Account.State.ONLINE) { - synchronized (account.inProgressConferenceJoins) { + if (account.getStatus() == Account.State.ONLINE) { + synchronized (account.inProgressConferenceJoins) { account.inProgressConferenceJoins.add(conversation); } if (Config.MUC_LEAVE_BEFORE_JOIN) { sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions())); } conversation.resetMucOptions(); - if (onConferenceJoined != null) { - conversation.getMucOptions().flagNoAutoPushConfiguration(); - } - conversation.setHasMessagesLeftOnServer(false); - fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() { + if (onConferenceJoined != null) { + conversation.getMucOptions().flagNoAutoPushConfiguration(); + } + conversation.setHasMessagesLeftOnServer(false); + fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() { - private void join(Conversation conversation) { - Account account = conversation.getAccount(); - final MucOptions mucOptions = conversation.getMucOptions(); + private void join(Conversation conversation) { + Account account = conversation.getAccount(); + final MucOptions mucOptions = conversation.getMucOptions(); - if (mucOptions.nonanonymous() && !mucOptions.membersOnly() && !conversation.getBooleanAttribute("accept_non_anonymous", false)) { - synchronized (account.inProgressConferenceJoins) { + if (mucOptions.nonanonymous() && !mucOptions.membersOnly() && !conversation.getBooleanAttribute("accept_non_anonymous", false)) { + synchronized (account.inProgressConferenceJoins) { account.inProgressConferenceJoins.remove(conversation); } - mucOptions.setError(MucOptions.Error.NON_ANONYMOUS); - updateConversationUi(); + mucOptions.setError(MucOptions.Error.NON_ANONYMOUS); + updateConversationUi(); if (onConferenceJoined != null) { onConferenceJoined.onConferenceJoined(conversation); } - return; + return; } - final Jid joinJid = mucOptions.getSelf().getFullJid(); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": joining conversation " + joinJid.toString()); - PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null); - packet.setTo(joinJid); - Element x = packet.addChild("x", "http://jabber.org/protocol/muc"); - if (conversation.getMucOptions().getPassword() != null) { - x.addChild("password").setContent(mucOptions.getPassword()); - } - - if (mucOptions.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().getTimestamp())); - } - sendPresencePacket(account, packet); - if (onConferenceJoined != null) { - onConferenceJoined.onConferenceJoined(conversation); - } - if (!joinJid.equals(conversation.getJid())) { - conversation.setContactJid(joinJid); - databaseBackend.updateConversation(conversation); - } - - if (mucOptions.mamSupport()) { - getMessageArchiveService().catchupMUC(conversation); - } - if (mucOptions.isPrivateAndNonAnonymous()) { - fetchConferenceMembers(conversation); - if (followedInvite && conversation.getBookmark() == null) { - saveConversationAsBookmark(conversation, null); - } - } - if (mucOptions.push()) { - enableMucPush(conversation); + final Jid joinJid = mucOptions.getSelf().getFullJid(); + Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": joining conversation " + joinJid.toString()); + PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null); + packet.setTo(joinJid); + Element x = packet.addChild("x", "http://jabber.org/protocol/muc"); + if (conversation.getMucOptions().getPassword() != null) { + x.addChild("password").setContent(mucOptions.getPassword()); } - synchronized (account.inProgressConferenceJoins) { + + if (mucOptions.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().getTimestamp())); + } + sendPresencePacket(account, packet); + if (onConferenceJoined != null) { + onConferenceJoined.onConferenceJoined(conversation); + } + if (!joinJid.equals(conversation.getJid())) { + conversation.setContactJid(joinJid); + databaseBackend.updateConversation(conversation); + } + + if (mucOptions.mamSupport()) { + getMessageArchiveService().catchupMUC(conversation); + } + if (mucOptions.isPrivateAndNonAnonymous()) { + fetchConferenceMembers(conversation); + + if (followedInvite) { + final Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + if (!bookmark.autojoin()) { + bookmark.setAutojoin(true); + createBookmark(account, bookmark); + } + } else { + saveConversationAsBookmark(conversation, null); + } + } + } + if (mucOptions.push()) { + enableMucPush(conversation); + } + synchronized (account.inProgressConferenceJoins) { account.inProgressConferenceJoins.remove(conversation); sendUnsentMessages(conversation); } - } + } - @Override - public void onConferenceConfigurationFetched(Conversation conversation) { - if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": conversation ("+conversation.getJid()+") got archived before IQ result"); - return; - } - join(conversation); - } - - @Override - public void onFetchFailed(final Conversation conversation, Element error) { + @Override + public void onConferenceConfigurationFetched(Conversation conversation) { if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": conversation ("+conversation.getJid()+") got archived before IQ result"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result"); + return; + } + join(conversation); + } + + @Override + public void onFetchFailed(final Conversation conversation, Element error) { + if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result"); return; } - if (error != null && "remote-server-not-found".equals(error.getName())) { - synchronized (account.inProgressConferenceJoins) { + if (error != null && "remote-server-not-found".equals(error.getName())) { + synchronized (account.inProgressConferenceJoins) { account.inProgressConferenceJoins.remove(conversation); } - conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND); - updateConversationUi(); - } else { - join(conversation); - fetchConferenceConfiguration(conversation); - } - } - }); - updateConversationUi(); - } else { - synchronized (account.pendingConferenceJoins) { + conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND); + updateConversationUi(); + } else { + join(conversation); + fetchConferenceConfiguration(conversation); + } + } + }); + updateConversationUi(); + } else { + synchronized (account.pendingConferenceJoins) { account.pendingConferenceJoins.add(conversation); } - conversation.resetMucOptions(); - conversation.setHasMessagesLeftOnServer(false); - updateConversationUi(); - } - } + conversation.resetMucOptions(); + conversation.setHasMessagesLeftOnServer(false); + updateConversationUi(); + } + } - private void enableDirectMucPush(final Conversation conversation) { + private void enableDirectMucPush(final Conversation conversation) { final Account account = conversation.getAccount(); final Jid room = conversation.getJid().asBareJid(); final IqPacket enable = mIqGenerator.enablePush(conversation.getAccount().getJid(), conversation.getUuid(), null); enable.setTo(room); sendIqPacket(account, enable, (a, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": enabled direct push for muc "+room); + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabled direct push for muc " + room); } else if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": unable to enable direct push for muc "+room+" "+response.getError()); + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to enable direct push for muc " + room + " " + response.getError()); } }); } - private void enableMucPush(final Conversation conversation) { - enableDirectMucPush(conversation); + private void enableMucPush(final Conversation conversation) { + enableDirectMucPush(conversation); mPushManagementService.registerPushTokenOnServer(conversation); } @@ -2685,230 +2754,231 @@ public class XmppConnectionService extends Service { disable.setTo(room); sendIqPacket(account, disable, (a, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": disabled direct push for muc "+room); + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": disabled direct push for muc " + room); } else if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": unable to disable direct push for muc "+room+" "+response.getError()); + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to disable direct push for muc " + room + " " + response.getError()); } }); } - private void fetchConferenceMembers(final Conversation conversation) { - final Account account = conversation.getAccount(); - final AxolotlService axolotlService = account.getAxolotlService(); - final String[] affiliations = {"member", "admin", "owner"}; - OnIqPacketReceived callback = new OnIqPacketReceived() { + private void fetchConferenceMembers(final Conversation conversation) { + final Account account = conversation.getAccount(); + final AxolotlService axolotlService = account.getAxolotlService(); + final String[] affiliations = {"member", "admin", "owner"}; + OnIqPacketReceived callback = new OnIqPacketReceived() { - private int i = 0; - private boolean success = true; + private int i = 0; + private boolean success = true; - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final boolean omemoEnabled = conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL; - Element query = packet.query("http://jabber.org/protocol/muc#admin"); - if (packet.getType() == IqPacket.TYPE.RESULT && query != null) { - for (Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - MucOptions.User user = AbstractParser.parseItem(conversation, child); - if (!user.realJidMatchesAccount()) { - boolean isNew = conversation.getMucOptions().updateUser(user); - Contact contact = user.getContact(); - if (omemoEnabled - && isNew - && user.getRealJid() != null - && (contact == null || !contact.mutualPresenceSubscription()) - && axolotlService.hasEmptyDeviceList(user.getRealJid())) { - axolotlService.fetchDeviceIds(user.getRealJid()); - } - } - } - } - } else { - success = false; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not request affiliation " + affiliations[i] + " in " + conversation.getJid().asBareJid()); - } - ++i; - if (i >= affiliations.length) { - List members = conversation.getMucOptions().getMembers(true); - if (success) { - List cryptoTargets = conversation.getAcceptedCryptoTargets(); - boolean changed = false; - for (ListIterator iterator = cryptoTargets.listIterator(); iterator.hasNext(); ) { - Jid jid = iterator.next(); - if (!members.contains(jid) && !members.contains(Jid.ofDomain(jid.getDomain()))) { - iterator.remove(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName()); - changed = true; - } - } - if (changed) { - conversation.setAcceptedCryptoTargets(cryptoTargets); - updateConversation(conversation); - } - } - getAvatarService().clear(conversation); - updateMucRosterUi(); - updateConversationUi(); - } - } - }; - for (String affiliation : affiliations) { - sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching members for " + conversation.getName()); - } + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + final boolean omemoEnabled = conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL; + Element query = packet.query("http://jabber.org/protocol/muc#admin"); + if (packet.getType() == IqPacket.TYPE.RESULT && query != null) { + for (Element child : query.getChildren()) { + if ("item".equals(child.getName())) { + MucOptions.User user = AbstractParser.parseItem(conversation, child); + if (!user.realJidMatchesAccount()) { + boolean isNew = conversation.getMucOptions().updateUser(user); + Contact contact = user.getContact(); + if (omemoEnabled + && isNew + && user.getRealJid() != null + && (contact == null || !contact.mutualPresenceSubscription()) + && axolotlService.hasEmptyDeviceList(user.getRealJid())) { + axolotlService.fetchDeviceIds(user.getRealJid()); + } + } + } + } + } else { + success = false; + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not request affiliation " + affiliations[i] + " in " + conversation.getJid().asBareJid()); + } + ++i; + if (i >= affiliations.length) { + List members = conversation.getMucOptions().getMembers(true); + if (success) { + List cryptoTargets = conversation.getAcceptedCryptoTargets(); + boolean changed = false; + for (ListIterator iterator = cryptoTargets.listIterator(); iterator.hasNext(); ) { + Jid jid = iterator.next(); + if (!members.contains(jid) && !members.contains(Jid.ofDomain(jid.getDomain()))) { + iterator.remove(); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName()); + changed = true; + } + } + if (changed) { + conversation.setAcceptedCryptoTargets(cryptoTargets); + updateConversation(conversation); + } + } + getAvatarService().clear(conversation); + updateMucRosterUi(); + updateConversationUi(); + } + } + }; + for (String affiliation : affiliations) { + sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching members for " + conversation.getName()); + } - public void providePasswordForMuc(Conversation conversation, String password) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - conversation.getMucOptions().setPassword(password); - if (conversation.getBookmark() != null) { - if (synchronizeWithBookmarks()) { - conversation.getBookmark().setAutojoin(true); - } - pushBookmarks(conversation.getAccount()); - } - updateConversation(conversation); - joinMuc(conversation); - } - } + public void providePasswordForMuc(Conversation conversation, String password) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + conversation.getMucOptions().setPassword(password); + if (conversation.getBookmark() != null) { + final Bookmark bookmark = conversation.getBookmark(); + if (synchronizeWithBookmarks()) { + bookmark.setAutojoin(true); + } + createBookmark(conversation.getAccount(), bookmark); + } + updateConversation(conversation); + joinMuc(conversation); + } + } - private boolean hasEnabledAccounts() { - if (this.accounts == null) { - return false; - } - for (Account account : this.accounts) { - if (account.isEnabled()) { - return true; - } - } - return false; - } + private boolean hasEnabledAccounts() { + if (this.accounts == null) { + return false; + } + for (Account account : this.accounts) { + if (account.isEnabled()) { + return true; + } + } + return false; + } - public void getAttachments(final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) { + public void getAttachments(final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) { getAttachments(conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded); } public void getAttachments(final Account account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) { - getAttachments(account.getUuid(),jid.asBareJid(),limit, onMediaLoaded); + getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded); } - public void getAttachments(final String account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) { + public void getAttachments(final String account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) { new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start(); } - public void persistSelfNick(MucOptions.User self) { - final Conversation conversation = self.getConversation(); - final boolean tookProposedNickFromBookmark = conversation.getMucOptions().isTookProposedNickFromBookmark(); - Jid full = self.getFullJid(); - if (!full.equals(conversation.getJid())) { - Log.d(Config.LOGTAG, "nick changed. updating"); - conversation.setContactJid(full); - databaseBackend.updateConversation(conversation); - } + public void persistSelfNick(MucOptions.User self) { + final Conversation conversation = self.getConversation(); + final boolean tookProposedNickFromBookmark = conversation.getMucOptions().isTookProposedNickFromBookmark(); + Jid full = self.getFullJid(); + if (!full.equals(conversation.getJid())) { + Log.d(Config.LOGTAG, "nick changed. updating"); + conversation.setContactJid(full); + databaseBackend.updateConversation(conversation); + } - final Bookmark bookmark = conversation.getBookmark(); - final String bookmarkedNick = bookmark == null ? null : bookmark.getNick(); + final Bookmark bookmark = conversation.getBookmark(); + final String bookmarkedNick = bookmark == null ? null : bookmark.getNick(); if (bookmark != null && (tookProposedNickFromBookmark || TextUtils.isEmpty(bookmarkedNick)) && !full.getResource().equals(bookmarkedNick)) { final Account account = conversation.getAccount(); final String defaultNick = MucOptions.defaultNick(account); if (TextUtils.isEmpty(bookmarkedNick) && full.getResource().equals(defaultNick)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": do not overwrite empty bookmark nick with default nick for "+conversation.getJid().asBareJid()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not overwrite empty bookmark nick with default nick for " + conversation.getJid().asBareJid()); return; } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persist nick '" + full.getResource() + "' into bookmark for " + conversation.getJid().asBareJid()); bookmark.setNick(full.getResource()); - pushBookmarks(bookmark.getAccount()); + createBookmark(bookmark.getAccount(), bookmark); } - } + } - public boolean renameInMuc(final Conversation conversation, final String nick, final UiCallback callback) { - final MucOptions options = conversation.getMucOptions(); - final Jid joinJid = options.createJoinJid(nick); - if (joinJid == null) { - return false; - } - if (options.online()) { - Account account = conversation.getAccount(); - options.setOnRenameListener(new OnRenameListener() { + public boolean renameInMuc(final Conversation conversation, final String nick, final UiCallback callback) { + final MucOptions options = conversation.getMucOptions(); + final Jid joinJid = options.createJoinJid(nick); + if (joinJid == null) { + return false; + } + if (options.online()) { + Account account = conversation.getAccount(); + options.setOnRenameListener(new OnRenameListener() { - @Override - public void onSuccess() { - callback.success(conversation); - } + @Override + public void onSuccess() { + callback.success(conversation); + } - @Override - public void onFailure() { - callback.error(R.string.nick_in_use, conversation); - } - }); + @Override + public void onFailure() { + callback.error(R.string.nick_in_use, conversation); + } + }); final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous()); packet.setTo(joinJid); - sendPresencePacket(account, packet); - } else { - conversation.setContactJid(joinJid); - databaseBackend.updateConversation(conversation); - if (conversation.getAccount().getStatus() == Account.State.ONLINE) { - Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - bookmark.setNick(nick); - pushBookmarks(bookmark.getAccount()); - } - joinMuc(conversation); - } - } - return true; - } + sendPresencePacket(account, packet); + } else { + conversation.setContactJid(joinJid); + databaseBackend.updateConversation(conversation); + if (conversation.getAccount().getStatus() == Account.State.ONLINE) { + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + bookmark.setNick(nick); + createBookmark(bookmark.getAccount(), bookmark); + } + joinMuc(conversation); + } + } + return true; + } - public void leaveMuc(Conversation conversation) { - leaveMuc(conversation, false); - } + public void leaveMuc(Conversation conversation) { + leaveMuc(conversation, false); + } - private void leaveMuc(Conversation conversation, boolean now) { - final Account account = conversation.getAccount(); - synchronized (account.pendingConferenceJoins) { + private void leaveMuc(Conversation conversation, boolean now) { + final Account account = conversation.getAccount(); + synchronized (account.pendingConferenceJoins) { account.pendingConferenceJoins.remove(conversation); } synchronized (account.pendingConferenceLeaves) { account.pendingConferenceLeaves.remove(conversation); } - if (account.getStatus() == Account.State.ONLINE || now) { - sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions())); - conversation.getMucOptions().setOffline(); - Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - bookmark.setConversation(null); - } - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid()); - } else { - synchronized (account.pendingConferenceLeaves) { + if (account.getStatus() == Account.State.ONLINE || now) { + sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions())); + conversation.getMucOptions().setOffline(); + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + bookmark.setConversation(null); + } + Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid()); + } else { + synchronized (account.pendingConferenceLeaves) { account.pendingConferenceLeaves.add(conversation); } - } - } + } + } - public String findConferenceServer(final Account account) { - String server; - if (account.getXmppConnection() != null) { - server = account.getXmppConnection().getMucServer(); - if (server != null) { - return server; - } - } - for (Account other : getAccounts()) { - if (other != account && other.getXmppConnection() != null) { - server = other.getXmppConnection().getMucServer(); - if (server != null) { - return server; - } - } - } - return null; - } + public String findConferenceServer(final Account account) { + String server; + if (account.getXmppConnection() != null) { + server = account.getXmppConnection().getMucServer(); + if (server != null) { + return server; + } + } + for (Account other : getAccounts()) { + if (other != account && other.getXmppConnection() != null) { + server = other.getXmppConnection().getMucServer(); + if (server != null) { + return server; + } + } + } + return null; + } - public void createPublicChannel(final Account account, final String name, final Jid address, final UiCallback callback) { + public void createPublicChannel(final Account account, final String name, final Jid address, final UiCallback callback) { joinMuc(findOrCreateConversation(account, address, true, false, true), conversation -> { final Bundle configuration = IqGenerator.defaultChannelConfiguration(); if (!TextUtils.isEmpty(name)) { @@ -2933,83 +3003,83 @@ public class XmppConnectionService extends Service { }); } - public boolean createAdhocConference(final Account account, - final String name, - final Iterable jids, - final UiCallback callback) { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": creating adhoc conference with " + jids.toString()); - if (account.getStatus() == Account.State.ONLINE) { - try { - String server = findConferenceServer(account); - if (server == null) { - if (callback != null) { - callback.error(R.string.no_conference_server_found, null); - } - return false; - } - final Jid jid = Jid.of(CryptoHelper.pronounceable(getRNG()), server, null); - final Conversation conversation = findOrCreateConversation(account, jid, true, false, true); - joinMuc(conversation, new OnConferenceJoined() { - @Override - public void onConferenceJoined(final Conversation conversation) { - final Bundle configuration = IqGenerator.defaultGroupChatConfiguration(); - if (!TextUtils.isEmpty(name)) { - configuration.putString("muc#roomconfig_roomname", name); - } - pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - for (Jid invite : jids) { - invite(conversation, invite); - } - for(String resource : account.getSelfContact().getPresences().toResourceArray()) { - Jid other = account.getJid().withResource(resource); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": sending direct invite to "+other); - directInvite(conversation, other); + public boolean createAdhocConference(final Account account, + final String name, + final Iterable jids, + final UiCallback callback) { + Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": creating adhoc conference with " + jids.toString()); + if (account.getStatus() == Account.State.ONLINE) { + try { + String server = findConferenceServer(account); + if (server == null) { + if (callback != null) { + callback.error(R.string.no_conference_server_found, null); + } + return false; + } + final Jid jid = Jid.of(CryptoHelper.pronounceable(getRNG()), server, null); + final Conversation conversation = findOrCreateConversation(account, jid, true, false, true); + joinMuc(conversation, new OnConferenceJoined() { + @Override + public void onConferenceJoined(final Conversation conversation) { + final Bundle configuration = IqGenerator.defaultGroupChatConfiguration(); + if (!TextUtils.isEmpty(name)) { + configuration.putString("muc#roomconfig_roomname", name); + } + pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + for (Jid invite : jids) { + invite(conversation, invite); } - saveConversationAsBookmark(conversation, name); - if (callback != null) { - callback.success(conversation); - } - } + for (String resource : account.getSelfContact().getPresences().toResourceArray()) { + Jid other = account.getJid().withResource(resource); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending direct invite to " + other); + directInvite(conversation, other); + } + saveConversationAsBookmark(conversation, name); + if (callback != null) { + callback.success(conversation); + } + } - @Override - public void onPushFailed() { - archiveConversation(conversation); - if (callback != null) { - callback.error(R.string.conference_creation_failed, conversation); - } - } - }); - } - }); - return true; - } catch (IllegalArgumentException e) { - if (callback != null) { - callback.error(R.string.conference_creation_failed, null); - } - return false; - } - } else { - if (callback != null) { - callback.error(R.string.not_connected_try_again, null); - } - return false; - } - } + @Override + public void onPushFailed() { + archiveConversation(conversation); + if (callback != null) { + callback.error(R.string.conference_creation_failed, conversation); + } + } + }); + } + }); + return true; + } catch (IllegalArgumentException e) { + if (callback != null) { + callback.error(R.string.conference_creation_failed, null); + } + return false; + } + } else { + if (callback != null) { + callback.error(R.string.not_connected_try_again, null); + } + return false; + } + } - public void fetchConferenceConfiguration(final Conversation conversation) { - fetchConferenceConfiguration(conversation, null); - } + 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().asBareJid()); - 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.RESULT) { + public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) { + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(conversation.getJid().asBareJid()); + 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.RESULT) { final MucOptions mucOptions = conversation.getMucOptions(); final Bookmark bookmark = conversation.getBookmark(); final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName()); @@ -3021,7 +3091,7 @@ public class XmppConnectionService extends Service { if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) { if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) { - pushBookmarks(account); + createBookmark(account, bookmark); } } @@ -3033,133 +3103,133 @@ public class XmppConnectionService extends Service { updateConversationUi(); } else if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received timeout waiting for conference configuration fetch"); - } else { - if (callback != null) { - callback.onFetchFailed(conversation, packet.getError()); - } - } - } - }); - } - - public void pushNodeConfiguration(Account account, final String node, final Bundle options, final OnConfigurationPushed callback) { - pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback); - } - - public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) { - Log.d(Config.LOGTAG,"pushing node configuration"); - sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); - Element configuration = pubsub == null ? null : pubsub.findChild("configure"); - Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA); - if (x != null) { - Data data = Data.parse(x); - data.submit(options); - sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT && callback != null) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": successfully changed node configuration for node "+node); - callback.onPushSucceeded(); - } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) { - callback.onPushFailed(); - } - } - }); - } else if (callback != null) { - callback.onPushFailed(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) { - callback.onPushFailed(); - } - } - }); - } - - public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConfigurationPushed callback) { - if (options.getString("muc#roomconfig_whois","moderators").equals("anyone")) { - conversation.setAttribute("accept_non_anonymous",true); - updateConversation(conversation); - } - IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.setTo(conversation.getJid().asBareJid()); - request.query("http://jabber.org/protocol/muc#owner"); - sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Data data = Data.parse(packet.query().findChild("x", Namespace.DATA)); - data.submit(options); - IqPacket set = new IqPacket(IqPacket.TYPE.SET); - set.setTo(conversation.getJid().asBareJid()); - set.query("http://jabber.org/protocol/muc#owner").addChild(data); - sendIqPacket(account, set, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (callback != null) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - callback.onPushSucceeded(); - } else { - callback.onPushFailed(); - } - } - } - }); - } else { - if (callback != null) { - callback.onPushFailed(); - } - } - } - }); - } - - public void pushSubjectToConference(final Conversation conference, final String subject) { - MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject)); - this.sendMessagePacket(conference.getAccount(), packet); - } - - public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) { - final Jid jid = user.asBareJid(); - IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); - sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - conference.getMucOptions().changeAffiliation(jid, affiliation); - getAvatarService().clear(conference); - callback.onAffiliationChangedSuccessful(jid); - } else { - callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation); - } - } - }); - } - - public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) { - List jids = new ArrayList<>(); - for (MucOptions.User user : conference.getMucOptions().getUsers()) { - if (user.getAffiliation() == before && user.getRealJid() != null) { - jids.add(user.getRealJid()); - } - } - IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString()); - sendIqPacket(conference.getAccount(), request, mDefaultIqHandler); - } - - public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) { - IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString()); - Log.d(Config.LOGTAG, request.toString()); - sendIqPacket(conference.getAccount(), request, (account, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+" unable to change role of "+nick); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch"); + } else { + if (callback != null) { + callback.onFetchFailed(conversation, packet.getError()); + } + } } }); - } + } + + public void pushNodeConfiguration(Account account, final String node, final Bundle options, final OnConfigurationPushed callback) { + pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback); + } + + public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) { + Log.d(Config.LOGTAG, "pushing node configuration"); + sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); + Element configuration = pubsub == null ? null : pubsub.findChild("configure"); + Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA); + if (x != null) { + Data data = Data.parse(x); + data.submit(options); + sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT && callback != null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully changed node configuration for node " + node); + callback.onPushSucceeded(); + } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) { + callback.onPushFailed(); + } + } + }); + } else if (callback != null) { + callback.onPushFailed(); + } + } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) { + callback.onPushFailed(); + } + } + }); + } + + public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConfigurationPushed callback) { + if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) { + conversation.setAttribute("accept_non_anonymous", true); + updateConversation(conversation); + } + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(conversation.getJid().asBareJid()); + request.query("http://jabber.org/protocol/muc#owner"); + sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Data data = Data.parse(packet.query().findChild("x", Namespace.DATA)); + data.submit(options); + IqPacket set = new IqPacket(IqPacket.TYPE.SET); + set.setTo(conversation.getJid().asBareJid()); + set.query("http://jabber.org/protocol/muc#owner").addChild(data); + sendIqPacket(account, set, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (callback != null) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + callback.onPushSucceeded(); + } else { + callback.onPushFailed(); + } + } + } + }); + } else { + if (callback != null) { + callback.onPushFailed(); + } + } + } + }); + } + + public void pushSubjectToConference(final Conversation conference, final String subject) { + MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject)); + this.sendMessagePacket(conference.getAccount(), packet); + } + + public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) { + final Jid jid = user.asBareJid(); + IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); + sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + conference.getMucOptions().changeAffiliation(jid, affiliation); + getAvatarService().clear(conference); + callback.onAffiliationChangedSuccessful(jid); + } else { + callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation); + } + } + }); + } + + public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) { + List jids = new ArrayList<>(); + for (MucOptions.User user : conference.getMucOptions().getUsers()) { + if (user.getAffiliation() == before && user.getRealJid() != null) { + jids.add(user.getRealJid()); + } + } + IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString()); + sendIqPacket(conference.getAccount(), request, mDefaultIqHandler); + } + + public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) { + IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString()); + Log.d(Config.LOGTAG, request.toString()); + sendIqPacket(conference.getAccount(), request, (account, packet) -> { + if (packet.getType() != IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick); + } + }); + } public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) { IqPacket request = new IqPacket(IqPacket.TYPE.SET); @@ -3181,158 +3251,158 @@ public class XmppConnectionService extends Service { }); } - private void disconnect(Account account, boolean force) { - if ((account.getStatus() == Account.State.ONLINE) - || (account.getStatus() == Account.State.DISABLED)) { - final XmppConnection connection = account.getXmppConnection(); - if (!force) { - List conversations = getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getAccount() == account) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - leaveMuc(conversation, true); - } - } - } - sendOfflinePresence(account); - } - connection.disconnect(force); - } - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - public void updateMessage(Message message) { - updateMessage(message, true); - } - - public void updateMessage(Message message, boolean includeBody) { - databaseBackend.updateMessage(message, includeBody); - updateConversationUi(); - } - - public void updateMessage(Message message, String uuid) { - if (!databaseBackend.updateMessage(message, uuid)) { - Log.e(Config.LOGTAG,"error updated message in DB after edit"); + private void disconnect(Account account, boolean force) { + if ((account.getStatus() == Account.State.ONLINE) + || (account.getStatus() == Account.State.DISABLED)) { + final XmppConnection connection = account.getXmppConnection(); + if (!force) { + List conversations = getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation, true); + } + } + } + sendOfflinePresence(account); + } + connection.disconnect(force); } - updateConversationUi(); - } + } - protected void syncDirtyContacts(Account account) { - for (Contact contact : account.getRoster().getContacts()) { - if (contact.getOption(Contact.Options.DIRTY_PUSH)) { - pushContactToServer(contact); - } - if (contact.getOption(Contact.Options.DIRTY_DELETE)) { - deleteContactOnServer(contact); - } - } - } + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } - public void createContact(Contact contact, boolean autoGrant) { - if (autoGrant) { - contact.setOption(Contact.Options.PREEMPTIVE_GRANT); - contact.setOption(Contact.Options.ASKING); - } - pushContactToServer(contact); - } + public void updateMessage(Message message) { + updateMessage(message, true); + } - public void pushContactToServer(final Contact contact) { - contact.resetOption(Contact.Options.DIRTY_DELETE); - contact.setOption(Contact.Options.DIRTY_PUSH); - final Account account = contact.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - final boolean ask = contact.getOption(Contact.Options.ASKING); - final boolean sendUpdates = contact - .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) - && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.query(Namespace.ROSTER).addChild(contact.asElement()); - account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); - if (sendUpdates) { - sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); - } - if (ask) { - sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact)); - } - } else { - syncRoster(contact.getAccount()); - } - } + public void updateMessage(Message message, boolean includeBody) { + databaseBackend.updateMessage(message, includeBody); + updateConversationUi(); + } - public void publishMucAvatar(final Conversation conversation, final Uri image, final OnAvatarPublication callback) { - new Thread(() -> { - final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; - final int size = Config.AVATAR_SIZE; - final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); - if (avatar != null) { - if (!getFileBackend().save(avatar)) { - callback.onAvatarPublicationFailed(R.string.error_saving_avatar); - return; - } - avatar.owner = conversation.getJid().asBareJid(); - publishMucAvatar(conversation, avatar, callback); - } else { - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting); - } - }).start(); - } + public void updateMessage(Message message, String uuid) { + if (!databaseBackend.updateMessage(message, uuid)) { + Log.e(Config.LOGTAG, "error updated message in DB after edit"); + } + updateConversationUi(); + } - public void publishAvatar(final Account account, final Uri image, final OnAvatarPublication callback) { - new Thread(() -> { - final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; - final int size = Config.AVATAR_SIZE; - final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); - if (avatar != null) { - if (!getFileBackend().save(avatar)) { - Log.d(Config.LOGTAG,"unable to save vcard"); - callback.onAvatarPublicationFailed(R.string.error_saving_avatar); - return; - } - publishAvatar(account, avatar, callback); - } else { - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting); - } - }).start(); + protected void syncDirtyContacts(Account account) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.getOption(Contact.Options.DIRTY_PUSH)) { + pushContactToServer(contact); + } + if (contact.getOption(Contact.Options.DIRTY_DELETE)) { + deleteContactOnServer(contact); + } + } + } - } + public void createContact(Contact contact, boolean autoGrant) { + if (autoGrant) { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + contact.setOption(Contact.Options.ASKING); + } + pushContactToServer(contact); + } - private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) { - final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar); - sendIqPacket(conversation.getAccount(), retrieve, (account, response) -> { - boolean itemNotFound = response.getType() == IqPacket.TYPE.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found"); - if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) { - Element vcard = response.findChild("vCard", "vcard-temp"); - if (vcard == null) { - vcard = new Element("vCard", "vcard-temp"); - } - Element photo = vcard.findChild("PHOTO"); - if (photo == null) { - photo = vcard.addChild("PHOTO"); - } - photo.clearChildren(); - photo.addChild("TYPE").setContent(avatar.type); - photo.addChild("BINVAL").setContent(avatar.image); - IqPacket publication = new IqPacket(IqPacket.TYPE.SET); - publication.setTo(conversation.getJid().asBareJid()); - publication.addChild(vcard); - sendIqPacket(account, publication, (a1, publicationResponse) -> { - if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { - callback.onAvatarPublicationSucceeded(); - } else { - Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getError()); - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); - } - }); - } else { - Log.d(Config.LOGTAG, "failed to request vcard " + response.toString()); - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support); - } - }); - } + public void pushContactToServer(final Contact contact) { + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.setOption(Contact.Options.DIRTY_PUSH); + final Account account = contact.getAccount(); + if (account.getStatus() == Account.State.ONLINE) { + final boolean ask = contact.getOption(Contact.Options.ASKING); + final boolean sendUpdates = contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) + && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); + final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + iq.query(Namespace.ROSTER).addChild(contact.asElement()); + account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); + if (sendUpdates) { + sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); + } + if (ask) { + sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact)); + } + } else { + syncRoster(contact.getAccount()); + } + } + + public void publishMucAvatar(final Conversation conversation, final Uri image, final OnAvatarPublication callback) { + new Thread(() -> { + final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; + final int size = Config.AVATAR_SIZE; + final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); + if (avatar != null) { + if (!getFileBackend().save(avatar)) { + callback.onAvatarPublicationFailed(R.string.error_saving_avatar); + return; + } + avatar.owner = conversation.getJid().asBareJid(); + publishMucAvatar(conversation, avatar, callback); + } else { + callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting); + } + }).start(); + } + + public void publishAvatar(final Account account, final Uri image, final OnAvatarPublication callback) { + new Thread(() -> { + final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; + final int size = Config.AVATAR_SIZE; + final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); + if (avatar != null) { + if (!getFileBackend().save(avatar)) { + Log.d(Config.LOGTAG, "unable to save vcard"); + callback.onAvatarPublicationFailed(R.string.error_saving_avatar); + return; + } + publishAvatar(account, avatar, callback); + } else { + callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting); + } + }).start(); + + } + + private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) { + final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar); + sendIqPacket(conversation.getAccount(), retrieve, (account, response) -> { + boolean itemNotFound = response.getType() == IqPacket.TYPE.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found"); + if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) { + Element vcard = response.findChild("vCard", "vcard-temp"); + if (vcard == null) { + vcard = new Element("vCard", "vcard-temp"); + } + Element photo = vcard.findChild("PHOTO"); + if (photo == null) { + photo = vcard.addChild("PHOTO"); + } + photo.clearChildren(); + photo.addChild("TYPE").setContent(avatar.type); + photo.addChild("BINVAL").setContent(avatar.image); + IqPacket publication = new IqPacket(IqPacket.TYPE.SET); + publication.setTo(conversation.getJid().asBareJid()); + publication.addChild(vcard); + sendIqPacket(account, publication, (a1, publicationResponse) -> { + if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { + callback.onAvatarPublicationSucceeded(); + } else { + Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getError()); + callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); + } + }); + } else { + Log.d(Config.LOGTAG, "failed to request vcard " + response.toString()); + callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support); + } + }); + } public void publishAvatar(Account account, final Avatar avatar, final OnAvatarPublication callback) { final Bundle options; @@ -3344,41 +3414,41 @@ public class XmppConnectionService extends Service { publishAvatar(account, avatar, options, true, callback); } - public void publishAvatar(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": publishing avatar. options="+options); - IqPacket packet = this.mIqGenerator.publishAvatar(avatar, options); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { + public void publishAvatar(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": publishing avatar. options=" + options); + IqPacket packet = this.mIqGenerator.publishAvatar(avatar, options); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - if (result.getType() == IqPacket.TYPE.RESULT) { - publishAvatarMetadata(account, avatar, options,true, callback); + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + if (result.getType() == IqPacket.TYPE.RESULT) { + publishAvatarMetadata(account, avatar, options, true, callback); } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration(account, "urn:xmpp:avatar:data", options, new OnConfigurationPushed() { + pushNodeConfiguration(account, "urn:xmpp:avatar:data", options, new OnConfigurationPushed() { @Override public void onPushSucceeded() { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": changed node configuration for avatar node"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar node"); publishAvatar(account, avatar, options, false, callback); } @Override public void onPushFailed() { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to change node configuration for avatar node"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to change node configuration for avatar node"); publishAvatar(account, avatar, null, false, callback); } }); - } else { - Element error = result.findChild("error"); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : "")); - if (callback != null) { - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); - } - } - } - }); - } + } else { + Element error = result.findChild("error"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : "")); + if (callback != null) { + callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); + } + } + } + }); + } - public void publishAvatarMetadata(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) { + public void publishAvatarMetadata(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) { final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options); sendIqPacket(account, packet, new OnIqPacketReceived() { @Override @@ -3397,14 +3467,14 @@ public class XmppConnectionService extends Service { pushNodeConfiguration(account, "urn:xmpp:avatar:metadata", options, new OnConfigurationPushed() { @Override public void onPushSucceeded() { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": changed node configuration for avatar meta data node"); - publishAvatarMetadata(account, avatar, options,false, callback); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar meta data node"); + publishAvatarMetadata(account, avatar, options, false, callback); } @Override public void onPushFailed() { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to change node configuration for avatar meta data node"); - publishAvatarMetadata(account, avatar, null,false, callback); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to change node configuration for avatar meta data node"); + publishAvatarMetadata(account, avatar, null, false, callback); } }); } else { @@ -3416,58 +3486,58 @@ public class XmppConnectionService extends Service { }); } - public void republishAvatarIfNeeded(Account account) { - if (account.getAxolotlService().isPepBroken()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken"); - return; - } - IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { + public void republishAvatarIfNeeded(Account account) { + if (account.getAxolotlService().isPepBroken()) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken"); + return; + } + IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { - private Avatar parseAvatar(IqPacket packet) { - Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - return Avatar.parseMetadata(items); - } - } - return null; - } + private Avatar parseAvatar(IqPacket packet) { + Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); + if (pubsub != null) { + Element items = pubsub.findChild("items"); + if (items != null) { + return Avatar.parseMetadata(items); + } + } + return null; + } - private boolean errorIsItemNotFound(IqPacket packet) { - Element error = packet.findChild("error"); - return packet.getType() == IqPacket.TYPE.ERROR - && error != null - && error.hasChild("item-not-found"); - } + private boolean errorIsItemNotFound(IqPacket packet) { + Element error = packet.findChild("error"); + return packet.getType() == IqPacket.TYPE.ERROR + && error != null + && error.hasChild("item-not-found"); + } - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) { - Avatar serverAvatar = parseAvatar(packet); - if (serverAvatar == null && account.getAvatar() != null) { - Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar()); - if (avatar != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar on server was null. republishing"); - publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null); - } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": error rereading avatar"); - } - } - } - } - }); - } + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) { + Avatar serverAvatar = parseAvatar(packet); + if (serverAvatar == null && account.getAvatar() != null) { + Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar()); + if (avatar != null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar on server was null. republishing"); + publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null); + } else { + Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": error rereading avatar"); + } + } + } + } + }); + } - public void fetchAvatar(Account account, Avatar avatar) { - fetchAvatar(account, avatar, null); - } + public void fetchAvatar(Account account, Avatar avatar) { + fetchAvatar(account, avatar, null); + } - public void fetchAvatar(Account account, final Avatar avatar, final UiCallback callback) { - final String KEY = generateFetchKey(account, avatar); - synchronized (this.mInProgressAvatarFetches) { - if (mInProgressAvatarFetches.add(KEY)) { + public void fetchAvatar(Account account, final Avatar avatar, final UiCallback callback) { + final String KEY = generateFetchKey(account, avatar); + synchronized (this.mInProgressAvatarFetches) { + if (mInProgressAvatarFetches.add(KEY)) { switch (avatar.origin) { case PEP: this.mInProgressAvatarFetches.add(KEY); @@ -3479,168 +3549,168 @@ public class XmppConnectionService extends Service { break; } } else if (avatar.origin == Avatar.Origin.PEP) { - mOmittedPepAvatarFetches.add(KEY); + mOmittedPepAvatarFetches.add(KEY); } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": already fetching "+avatar.origin+" avatar for "+avatar.owner); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": already fetching " + avatar.origin + " avatar for " + avatar.owner); } - } - } + } + } - private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar); - sendIqPacket(account, packet, (a, result) -> { - synchronized (mInProgressAvatarFetches) { - mInProgressAvatarFetches.remove(generateFetchKey(a, avatar)); - } - final String ERROR = a.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed "; - if (result.getType() == IqPacket.TYPE.RESULT) { - avatar.image = mIqParser.avatarData(result); - if (avatar.image != null) { - if (getFileBackend().save(avatar)) { - if (a.getJid().asBareJid().equals(avatar.owner)) { - if (a.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(a); - } - getAvatarService().clear(a); - updateConversationUi(); - updateAccountUi(); - } else { - Contact contact = a.getRoster().getContact(avatar.owner); - if (contact.setAvatar(avatar)) { - syncRoster(account); - getAvatarService().clear(contact); - updateConversationUi(); - updateRosterUi(); - } - } - if (callback != null) { - callback.success(avatar); - } - Log.d(Config.LOGTAG, a.getJid().asBareJid() - + ": successfully fetched pep avatar for " + avatar.owner); - return; - } - } else { + private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback callback) { + IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar); + sendIqPacket(account, packet, (a, result) -> { + synchronized (mInProgressAvatarFetches) { + mInProgressAvatarFetches.remove(generateFetchKey(a, avatar)); + } + final String ERROR = a.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed "; + if (result.getType() == IqPacket.TYPE.RESULT) { + avatar.image = mIqParser.avatarData(result); + if (avatar.image != null) { + if (getFileBackend().save(avatar)) { + if (a.getJid().asBareJid().equals(avatar.owner)) { + if (a.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(a); + } + getAvatarService().clear(a); + updateConversationUi(); + updateAccountUi(); + } else { + Contact contact = a.getRoster().getContact(avatar.owner); + if (contact.setAvatar(avatar)) { + syncRoster(account); + getAvatarService().clear(contact); + updateConversationUi(); + updateRosterUi(); + } + } + if (callback != null) { + callback.success(avatar); + } + Log.d(Config.LOGTAG, a.getJid().asBareJid() + + ": successfully fetched pep avatar for " + avatar.owner); + return; + } + } else { - Log.d(Config.LOGTAG, ERROR + "(parsing error)"); - } - } else { - Element error = result.findChild("error"); - if (error == null) { - Log.d(Config.LOGTAG, ERROR + "(server error)"); - } else { - Log.d(Config.LOGTAG, ERROR + error.toString()); - } - } - if (callback != null) { - callback.error(0, null); - } + Log.d(Config.LOGTAG, ERROR + "(parsing error)"); + } + } else { + Element error = result.findChild("error"); + if (error == null) { + Log.d(Config.LOGTAG, ERROR + "(server error)"); + } else { + Log.d(Config.LOGTAG, ERROR + error.toString()); + } + } + if (callback != null) { + callback.error(0, null); + } - }); - } + }); + } - private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final boolean previouslyOmittedPepFetch; - synchronized (mInProgressAvatarFetches) { - final String KEY = generateFetchKey(account, avatar); - mInProgressAvatarFetches.remove(KEY); - previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY); - } - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element vCard = packet.findChild("vCard", "vcard-temp"); - Element photo = vCard != null ? vCard.findChild("PHOTO") : null; - String image = photo != null ? photo.findChildContent("BINVAL") : null; - if (image != null) { - avatar.image = image; - if (getFileBackend().save(avatar)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": successfully fetched vCard avatar for " + avatar.owner+" omittedPep="+previouslyOmittedPepFetch); - if (avatar.owner.isBareJid()) { - if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": had no avatar. replacing with vcard"); - account.setAvatar(avatar.getFilename()); - databaseBackend.updateAccount(account); - getAvatarService().clear(account); - updateAccountUi(); - } else { - Contact contact = account.getRoster().getContact(avatar.owner); - if (contact.setAvatar(avatar, previouslyOmittedPepFetch)) { - syncRoster(account); - getAvatarService().clear(contact); - updateRosterUi(); - } - } - updateConversationUi(); - } else { - Conversation conversation = find(account, avatar.owner.asBareJid()); - if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { - MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner); - if (user != null) { - if (user.setAvatar(avatar)) { - getAvatarService().clear(user); - updateConversationUi(); - updateMucRosterUi(); - } - if (user.getRealJid() != null) { - Contact contact = account.getRoster().getContact(user.getRealJid()); - if (contact.setAvatar(avatar)) { + private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback callback) { + IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + final boolean previouslyOmittedPepFetch; + synchronized (mInProgressAvatarFetches) { + final String KEY = generateFetchKey(account, avatar); + mInProgressAvatarFetches.remove(KEY); + previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY); + } + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element vCard = packet.findChild("vCard", "vcard-temp"); + Element photo = vCard != null ? vCard.findChild("PHOTO") : null; + String image = photo != null ? photo.findChildContent("BINVAL") : null; + if (image != null) { + avatar.image = image; + if (getFileBackend().save(avatar)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + + ": successfully fetched vCard avatar for " + avatar.owner + " omittedPep=" + previouslyOmittedPepFetch); + if (avatar.owner.isBareJid()) { + if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": had no avatar. replacing with vcard"); + account.setAvatar(avatar.getFilename()); + databaseBackend.updateAccount(account); + getAvatarService().clear(account); + updateAccountUi(); + } else { + Contact contact = account.getRoster().getContact(avatar.owner); + if (contact.setAvatar(avatar, previouslyOmittedPepFetch)) { + syncRoster(account); + getAvatarService().clear(contact); + updateRosterUi(); + } + } + updateConversationUi(); + } else { + Conversation conversation = find(account, avatar.owner.asBareJid()); + if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { + MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner); + if (user != null) { + if (user.setAvatar(avatar)) { + getAvatarService().clear(user); + updateConversationUi(); + updateMucRosterUi(); + } + if (user.getRealJid() != null) { + Contact contact = account.getRoster().getContact(user.getRealJid()); + if (contact.setAvatar(avatar)) { syncRoster(account); getAvatarService().clear(contact); updateRosterUi(); } } - } - } - } - } - } - } - } - }); - } + } + } + } + } + } + } + } + }); + } - public void checkForAvatar(Account account, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { + public void checkForAvatar(Account account, final UiCallback callback) { + IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - Avatar avatar = Avatar.parseMetadata(items); - if (avatar != null) { - avatar.owner = account.getJid().asBareJid(); - if (fileBackend.isAvatarCached(avatar)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); - } - getAvatarService().clear(account); - callback.success(avatar); - } else { - fetchAvatarPep(account, avatar, callback); - } - return; - } - } - } - } - callback.error(0, null); - } - }); - } + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); + if (pubsub != null) { + Element items = pubsub.findChild("items"); + if (items != null) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar != null) { + avatar.owner = account.getJid().asBareJid(); + if (fileBackend.isAvatarCached(avatar)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + getAvatarService().clear(account); + callback.success(avatar); + } else { + fetchAvatarPep(account, avatar, callback); + } + return; + } + } + } + } + callback.error(0, null); + } + }); + } - public void notifyAccountAvatarHasChanged(final Account account) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null && connection.getFeatures().bookmarksConversion()) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": avatar changed. resending presence to online group chats"); - for(Conversation conversation : conversations) { + public void notifyAccountAvatarHasChanged(final Account account) { + final XmppConnection connection = account.getXmppConnection(); + if (connection != null && connection.getFeatures().bookmarksConversion()) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar changed. resending presence to online group chats"); + for (Conversation conversation : conversations) { if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) { final MucOptions mucOptions = conversation.getMucOptions(); if (mucOptions.online()) { @@ -3653,872 +3723,871 @@ public class XmppConnectionService extends Service { } } - public void deleteContactOnServer(Contact contact) { - contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - contact.resetOption(Contact.Options.DIRTY_PUSH); - contact.setOption(Contact.Options.DIRTY_DELETE); - Account account = contact.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - Element item = iq.query(Namespace.ROSTER).addChild("item"); - item.setAttribute("jid", contact.getJid().toString()); - item.setAttribute("subscription", "remove"); - account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); - } - } - - public void updateConversation(final Conversation conversation) { - mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation)); - } - - private void reconnectAccount(final Account account, final boolean force, final boolean interactive) { - synchronized (account) { - XmppConnection connection = account.getXmppConnection(); - if (connection == null) { - connection = createConnection(account); - account.setXmppConnection(connection); - } - boolean hasInternet = hasInternetConnection(); - if (account.isEnabled() && hasInternet) { - if (!force) { - disconnect(account, false); - } - Thread thread = new Thread(connection); - connection.setInteractive(interactive); - connection.prepareNewConnection(); - connection.interrupt(); - thread.start(); - scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); - } else { - disconnect(account, force || account.getTrueStatus().isError() || !hasInternet); - account.getRoster().clearPresences(); - connection.resetEverything(); - final AxolotlService axolotlService = account.getAxolotlService(); - if (axolotlService != null) { - axolotlService.resetBrokenness(); - } - if (!hasInternet) { - account.setStatus(Account.State.NO_INTERNET); - } - } - } - } - - public void reconnectAccountInBackground(final Account account) { - new Thread(() -> reconnectAccount(account, false, true)).start(); - } - - public void invite(Conversation conversation, Jid contact) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid()); - MessagePacket packet = mMessageGenerator.invite(conversation, contact); - sendMessagePacket(conversation.getAccount(), packet); - } - - public void directInvite(Conversation conversation, Jid jid) { - MessagePacket packet = mMessageGenerator.directInvite(conversation, jid); - sendMessagePacket(conversation.getAccount(), packet); - } - - public void resetSendingToWaiting(Account account) { - for (Conversation conversation : getConversations()) { - if (conversation.getAccount() == account) { - conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); - } - } - } - - public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) { - return markMessage(account, recipient, uuid, status, null); - } - - public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status, String errorMessage) { - if (uuid == null) { - return null; - } - for (Conversation conversation : getConversations()) { - if (conversation.getJid().asBareJid().equals(recipient) && conversation.getAccount() == account) { - final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid); - if (message != null) { - markMessage(message, status, errorMessage); - } - return message; - } - } - return null; - } - - public boolean markMessage(Conversation conversation, String uuid, int status, String serverMessageId) { - if (uuid == null) { - return false; - } else { - Message message = conversation.findSentMessageWithUuid(uuid); - if (message != null) { - if (message.getServerMsgId() == null) { - message.setServerMsgId(serverMessageId); - } - markMessage(message, status); - return true; - } else { - return false; - } - } - } - - public void markMessage(Message message, int status) { - markMessage(message, status, null); - } - - - public void markMessage(Message message, int status, String errorMessage) { - final int c = message.getStatus(); - if (status == Message.STATUS_SEND_FAILED && (c == Message.STATUS_SEND_RECEIVED || c == Message.STATUS_SEND_DISPLAYED)) { - return; - } - if (status == Message.STATUS_SEND_RECEIVED && c == Message.STATUS_SEND_DISPLAYED) { - return; - } - message.setErrorMessage(errorMessage); - message.setStatus(status); - databaseBackend.updateMessage(message, false); - updateConversationUi(); - } - - private SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - } - - public long getAutomaticMessageDeletionDate() { - final long timeout = getLongPreference(SettingsActivity.AUTOMATIC_MESSAGE_DELETION, R.integer.automatic_message_deletion); - return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000)); - } - - public long getLongPreference(String name, @IntegerRes int res) { - long defaultValue = getResources().getInteger(res); - try { - return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue))); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - public boolean getBooleanPreference(String name, @BoolRes int res) { - return getPreferences().getBoolean(name, getResources().getBoolean(res)); - } - - public boolean confirmMessages() { - return getBooleanPreference("confirm_messages", R.bool.confirm_messages); - } - - public boolean allowMessageCorrection() { - return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction); - } - - public boolean sendChatStates() { - return getBooleanPreference("chat_states", R.bool.chat_states); - } - - private boolean synchronizeWithBookmarks() { - return getBooleanPreference("autojoin", R.bool.autojoin); - } - - public boolean indicateReceived() { - return getBooleanPreference("indicate_received", R.bool.indicate_received); - } - - public boolean useTorToConnect() { - return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor); - } - - public boolean showExtendedConnectionOptions() { - return QuickConversationsService.isConversations() && getBooleanPreference("show_connection_options", R.bool.show_connection_options); - } - - public boolean broadcastLastActivity() { - return getBooleanPreference(SettingsActivity.BROADCAST_LAST_ACTIVITY, R.bool.last_activity); - } - - public int unreadCount() { - int count = 0; - for (Conversation conversation : getConversations()) { - count += conversation.unreadCount(); - } - return count; - } - - - private List threadSafeList(Set set) { - synchronized (LISTENER_LOCK) { - return set.size() == 0 ? Collections.emptyList() : new ArrayList<>(set); - } - } - - public void showErrorToastInUi(int resId) { - for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) { - listener.onShowErrorToast(resId); - } - } - - public void updateConversationUi() { - for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) { - listener.onConversationUpdate(); - } - } - - public void updateAccountUi() { - for (OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) { - listener.onAccountUpdate(); - } - } - - public void updateRosterUi() { - for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) { - listener.onRosterUpdate(); - } - } - - public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) { - if (mOnCaptchaRequested.size() > 0) { - DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics(); - Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity), - (int) (captcha.getHeight() * metrics.scaledDensity), false); - for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) { - listener.onCaptchaRequested(account, id, data, scaled); - } - return true; - } - return false; - } - - public void updateBlocklistUi(final OnUpdateBlocklist.Status status) { - for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) { - listener.OnUpdateBlocklist(status); - } - } - - public void updateMucRosterUi() { - for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) { - listener.onMucRosterUpdate(); - } - } - - public void keyStatusUpdated(AxolotlService.FetchStatus report) { - for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) { - listener.onKeyStatusUpdated(report); - } - } - - public Account findAccountByJid(final Jid accountJid) { - for (Account account : this.accounts) { - if (account.getJid().asBareJid().equals(accountJid.asBareJid())) { - return account; - } - } - return null; - } - - public Account findAccountByUuid(final String uuid) { - for(Account account : this.accounts) { - if (account.getUuid().equals(uuid)) { - return account; - } - } - return null; - } - - public Conversation findConversationByUuid(String uuid) { - for (Conversation conversation : getConversations()) { - if (conversation.getUuid().equals(uuid)) { - return conversation; - } - } - return null; - } - - public Conversation findUniqueConversationByJid(XmppUri xmppUri) { - List findings = new ArrayList<>(); - for (Conversation c : getConversations()) { - if (c.getAccount().isEnabled() && c.getJid().asBareJid().equals(xmppUri.getJid()) && ((c.getMode() == Conversational.MODE_MULTI) == xmppUri.isAction(XmppUri.ACTION_JOIN))) { - findings.add(c); - } - } - return findings.size() == 1 ? findings.get(0) : null; - } - - public boolean markRead(final Conversation conversation, boolean dismiss) { - return markRead(conversation, null, dismiss).size() > 0; - } - - public void markRead(final Conversation conversation) { - markRead(conversation, null, true); - } - - public List markRead(final Conversation conversation, String upToUuid, boolean dismiss) { - if (dismiss) { - mNotificationService.clear(conversation); - } - final List readMessages = conversation.markRead(upToUuid); - if (readMessages.size() > 0) { - Runnable runnable = () -> { - for (Message message : readMessages) { - databaseBackend.updateMessage(message, false); - } - }; - mDatabaseWriterExecutor.execute(runnable); - updateUnreadCountBadge(); - return readMessages; - } else { - return readMessages; - } - } - - public synchronized void updateUnreadCountBadge() { - int count = unreadCount(); - if (unreadCount != count) { - Log.d(Config.LOGTAG, "update unread count to " + count); - if (count > 0) { - ShortcutBadger.applyCount(getApplicationContext(), count); - } else { - ShortcutBadger.removeCount(getApplicationContext()); - } - unreadCount = count; - } - } - - public void sendReadMarker(final Conversation conversation, String upToUuid) { - final boolean isPrivateAndNonAnonymousMuc = conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous(); - final List readMessages = this.markRead(conversation, upToUuid, true); - if (readMessages.size() > 0) { - updateConversationUi(); - } - final Message markable = Conversation.getLatestMarkableMessage(readMessages, isPrivateAndNonAnonymousMuc); - if (confirmMessages() - && markable != null - && (markable.trusted() || isPrivateAndNonAnonymousMuc) - && markable.getRemoteMsgId() != null) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); - Account account = conversation.getAccount(); - final Jid to = markable.getCounterpart(); - final boolean groupChat = conversation.getMode() == Conversation.MODE_MULTI; - MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId(), markable.getCounterpart(), groupChat); - this.sendMessagePacket(conversation.getAccount(), packet); - } - } - - public SecureRandom getRNG() { - return this.mRandom; - } - - public MemorizingTrustManager getMemorizingTrustManager() { - return this.mMemorizingTrustManager; - } - - public void setMemorizingTrustManager(MemorizingTrustManager trustManager) { - this.mMemorizingTrustManager = trustManager; - } - - public void updateMemorizingTrustmanager() { - final MemorizingTrustManager tm; - final boolean dontTrustSystemCAs = getBooleanPreference("dont_trust_system_cas", R.bool.dont_trust_system_cas); - if (dontTrustSystemCAs) { - tm = new MemorizingTrustManager(getApplicationContext(), null); - } else { - tm = new MemorizingTrustManager(getApplicationContext()); - } - setMemorizingTrustManager(tm); - } - - public LruCache getBitmapCache() { - return this.mBitmapCache; - } - - public Collection getKnownHosts() { - final Set hosts = new HashSet<>(); - for (final Account account : getAccounts()) { - hosts.add(account.getServer()); - for (final Contact contact : account.getRoster().getContacts()) { - if (contact.showInRoster()) { - final String server = contact.getServer(); - if (server != null) { - hosts.add(server); - } - } - } - } - if (Config.QUICKSY_DOMAIN != null) { - hosts.remove(Config.QUICKSY_DOMAIN); //we only want to show this when we type a e164 number + public void deleteContactOnServer(Contact contact) { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.setOption(Contact.Options.DIRTY_DELETE); + Account account = contact.getAccount(); + if (account.getStatus() == Account.State.ONLINE) { + IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + Element item = iq.query(Namespace.ROSTER).addChild("item"); + item.setAttribute("jid", contact.getJid().toString()); + item.setAttribute("subscription", "remove"); + account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); } - if (Config.DOMAIN_LOCK != null) { - hosts.add(Config.DOMAIN_LOCK); - } - if (Config.MAGIC_CREATE_DOMAIN != null) { - hosts.add(Config.MAGIC_CREATE_DOMAIN); - } - return hosts; - } + } - public Collection getKnownConferenceHosts() { - final Set mucServers = new HashSet<>(); - for (final Account account : accounts) { - if (account.getXmppConnection() != null) { - mucServers.addAll(account.getXmppConnection().getMucServers()); - for (Bookmark bookmark : account.getBookmarks()) { - final Jid jid = bookmark.getJid(); - final String s = jid == null ? null : jid.getDomain(); - if (s != null) { - mucServers.add(s); - } - } - } - } - return mucServers; - } + public void updateConversation(final Conversation conversation) { + mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation)); + } - public void sendMessagePacket(Account account, MessagePacket packet) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendMessagePacket(packet); - } - } - - public void sendPresencePacket(Account account, PresencePacket packet) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendPresencePacket(packet); - } - } - - public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data); - connection.sendUnmodifiedIqPacket(request, connection.registrationResponseListener, true); - } - } - - public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendIqPacket(packet, callback); - } else if (callback != null) { - callback.onIqPacketReceived(account,new IqPacket(IqPacket.TYPE.TIMEOUT)); + private void reconnectAccount(final Account account, final boolean force, final boolean interactive) { + synchronized (account) { + XmppConnection connection = account.getXmppConnection(); + if (connection == null) { + connection = createConnection(account); + account.setXmppConnection(connection); + } + boolean hasInternet = hasInternetConnection(); + if (account.isEnabled() && hasInternet) { + if (!force) { + disconnect(account, false); + } + Thread thread = new Thread(connection); + connection.setInteractive(interactive); + connection.prepareNewConnection(); + connection.interrupt(); + thread.start(); + scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); + } else { + disconnect(account, force || account.getTrueStatus().isError() || !hasInternet); + account.getRoster().clearPresences(); + connection.resetEverything(); + final AxolotlService axolotlService = account.getAxolotlService(); + if (axolotlService != null) { + axolotlService.resetBrokenness(); + } + if (!hasInternet) { + account.setStatus(Account.State.NO_INTERNET); + } + } } - } + } - public void sendPresence(final Account account) { - sendPresence(account, checkListeners() && broadcastLastActivity()); - } + public void reconnectAccountInBackground(final Account account) { + new Thread(() -> reconnectAccount(account, false, true)).start(); + } - private void sendPresence(final Account account, final boolean includeIdleTimestamp) { - Presence.Status status; - if (manuallyChangePresence()) { - status = account.getPresenceStatus(); - } else { - status = getTargetPresence(); - } - final PresencePacket packet = mPresenceGenerator.selfPresence(account, status); - if (mLastActivity > 0 && includeIdleTimestamp) { - long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates - packet.addChild("idle", Namespace.IDLE).setAttribute("since", AbstractGenerator.getTimestamp(since)); - } - sendPresencePacket(account, packet); - } + public void invite(Conversation conversation, Jid contact) { + Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid()); + MessagePacket packet = mMessageGenerator.invite(conversation, contact); + sendMessagePacket(conversation.getAccount(), packet); + } - private void deactivateGracePeriod() { - for (Account account : getAccounts()) { - account.deactivateGracePeriod(); - } - } + public void directInvite(Conversation conversation, Jid jid) { + MessagePacket packet = mMessageGenerator.directInvite(conversation, jid); + sendMessagePacket(conversation.getAccount(), packet); + } - public void refreshAllPresences() { - boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity(); - for (Account account : getAccounts()) { - if (account.isEnabled()) { - sendPresence(account, includeIdleTimestamp); - } - } - } + public void resetSendingToWaiting(Account account) { + for (Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); + } + } + } - private void refreshAllFcmTokens() { - for (Account account : getAccounts()) { - if (account.isOnlineAndConnected() && mPushManagementService.available(account)) { - mPushManagementService.registerPushTokenOnServer(account); - //TODO renew mucs - } - } - } + public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) { + return markMessage(account, recipient, uuid, status, null); + } - private void sendOfflinePresence(final Account account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence"); - sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); - } + public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status, String errorMessage) { + if (uuid == null) { + return null; + } + for (Conversation conversation : getConversations()) { + if (conversation.getJid().asBareJid().equals(recipient) && conversation.getAccount() == account) { + final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid); + if (message != null) { + markMessage(message, status, errorMessage); + } + return message; + } + } + return null; + } - public MessageGenerator getMessageGenerator() { - return this.mMessageGenerator; - } + public boolean markMessage(Conversation conversation, String uuid, int status, String serverMessageId) { + if (uuid == null) { + return false; + } else { + Message message = conversation.findSentMessageWithUuid(uuid); + if (message != null) { + if (message.getServerMsgId() == null) { + message.setServerMsgId(serverMessageId); + } + markMessage(message, status); + return true; + } else { + return false; + } + } + } - public PresenceGenerator getPresenceGenerator() { - return this.mPresenceGenerator; - } + public void markMessage(Message message, int status) { + markMessage(message, status, null); + } - public IqGenerator getIqGenerator() { - return this.mIqGenerator; - } - public IqParser getIqParser() { - return this.mIqParser; - } + public void markMessage(Message message, int status, String errorMessage) { + final int oldStatus = message.getStatus(); + if (status == Message.STATUS_SEND_FAILED && (oldStatus == Message.STATUS_SEND_RECEIVED || oldStatus == Message.STATUS_SEND_DISPLAYED)) { + return; + } + if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) { + return; + } + message.setErrorMessage(errorMessage); + message.setStatus(status); + databaseBackend.updateMessage(message, false); + updateConversationUi(); + } - public JingleConnectionManager getJingleConnectionManager() { - return this.mJingleConnectionManager; - } + private SharedPreferences getPreferences() { + return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + } - public MessageArchiveService getMessageArchiveService() { - return this.mMessageArchiveService; - } + public long getAutomaticMessageDeletionDate() { + final long timeout = getLongPreference(SettingsActivity.AUTOMATIC_MESSAGE_DELETION, R.integer.automatic_message_deletion); + return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000)); + } - public QuickConversationsService getQuickConversationsService() { + public long getLongPreference(String name, @IntegerRes int res) { + long defaultValue = getResources().getInteger(res); + try { + return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue))); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + public boolean getBooleanPreference(String name, @BoolRes int res) { + return getPreferences().getBoolean(name, getResources().getBoolean(res)); + } + + public boolean confirmMessages() { + return getBooleanPreference("confirm_messages", R.bool.confirm_messages); + } + + public boolean allowMessageCorrection() { + return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction); + } + + public boolean sendChatStates() { + return getBooleanPreference("chat_states", R.bool.chat_states); + } + + private boolean synchronizeWithBookmarks() { + return getBooleanPreference("autojoin", R.bool.autojoin); + } + + public boolean indicateReceived() { + return getBooleanPreference("indicate_received", R.bool.indicate_received); + } + + public boolean useTorToConnect() { + return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor); + } + + public boolean showExtendedConnectionOptions() { + return QuickConversationsService.isConversations() && getBooleanPreference("show_connection_options", R.bool.show_connection_options); + } + + public boolean broadcastLastActivity() { + return getBooleanPreference(SettingsActivity.BROADCAST_LAST_ACTIVITY, R.bool.last_activity); + } + + public int unreadCount() { + int count = 0; + for (Conversation conversation : getConversations()) { + count += conversation.unreadCount(); + } + return count; + } + + + private List threadSafeList(Set set) { + synchronized (LISTENER_LOCK) { + return set.size() == 0 ? Collections.emptyList() : new ArrayList<>(set); + } + } + + public void showErrorToastInUi(int resId) { + for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) { + listener.onShowErrorToast(resId); + } + } + + public void updateConversationUi() { + for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) { + listener.onConversationUpdate(); + } + } + + public void updateAccountUi() { + for (OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) { + listener.onAccountUpdate(); + } + } + + public void updateRosterUi() { + for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) { + listener.onRosterUpdate(); + } + } + + public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) { + if (mOnCaptchaRequested.size() > 0) { + DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics(); + Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity), + (int) (captcha.getHeight() * metrics.scaledDensity), false); + for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) { + listener.onCaptchaRequested(account, id, data, scaled); + } + return true; + } + return false; + } + + public void updateBlocklistUi(final OnUpdateBlocklist.Status status) { + for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) { + listener.OnUpdateBlocklist(status); + } + } + + public void updateMucRosterUi() { + for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) { + listener.onMucRosterUpdate(); + } + } + + public void keyStatusUpdated(AxolotlService.FetchStatus report) { + for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) { + listener.onKeyStatusUpdated(report); + } + } + + public Account findAccountByJid(final Jid accountJid) { + for (Account account : this.accounts) { + if (account.getJid().asBareJid().equals(accountJid.asBareJid())) { + return account; + } + } + return null; + } + + public Account findAccountByUuid(final String uuid) { + for (Account account : this.accounts) { + if (account.getUuid().equals(uuid)) { + return account; + } + } + return null; + } + + public Conversation findConversationByUuid(String uuid) { + for (Conversation conversation : getConversations()) { + if (conversation.getUuid().equals(uuid)) { + return conversation; + } + } + return null; + } + + public Conversation findUniqueConversationByJid(XmppUri xmppUri) { + List findings = new ArrayList<>(); + for (Conversation c : getConversations()) { + if (c.getAccount().isEnabled() && c.getJid().asBareJid().equals(xmppUri.getJid()) && ((c.getMode() == Conversational.MODE_MULTI) == xmppUri.isAction(XmppUri.ACTION_JOIN))) { + findings.add(c); + } + } + return findings.size() == 1 ? findings.get(0) : null; + } + + public boolean markRead(final Conversation conversation, boolean dismiss) { + return markRead(conversation, null, dismiss).size() > 0; + } + + public void markRead(final Conversation conversation) { + markRead(conversation, null, true); + } + + public List markRead(final Conversation conversation, String upToUuid, boolean dismiss) { + if (dismiss) { + mNotificationService.clear(conversation); + } + final List readMessages = conversation.markRead(upToUuid); + if (readMessages.size() > 0) { + Runnable runnable = () -> { + for (Message message : readMessages) { + databaseBackend.updateMessage(message, false); + } + }; + mDatabaseWriterExecutor.execute(runnable); + updateUnreadCountBadge(); + return readMessages; + } else { + return readMessages; + } + } + + public synchronized void updateUnreadCountBadge() { + int count = unreadCount(); + if (unreadCount != count) { + Log.d(Config.LOGTAG, "update unread count to " + count); + if (count > 0) { + ShortcutBadger.applyCount(getApplicationContext(), count); + } else { + ShortcutBadger.removeCount(getApplicationContext()); + } + unreadCount = count; + } + } + + public void sendReadMarker(final Conversation conversation, String upToUuid) { + final boolean isPrivateAndNonAnonymousMuc = conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous(); + final List readMessages = this.markRead(conversation, upToUuid, true); + if (readMessages.size() > 0) { + updateConversationUi(); + } + final Message markable = Conversation.getLatestMarkableMessage(readMessages, isPrivateAndNonAnonymousMuc); + if (confirmMessages() + && markable != null + && (markable.trusted() || isPrivateAndNonAnonymousMuc) + && markable.getRemoteMsgId() != null) { + Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); + Account account = conversation.getAccount(); + final Jid to = markable.getCounterpart(); + final boolean groupChat = conversation.getMode() == Conversation.MODE_MULTI; + MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId(), markable.getCounterpart(), groupChat); + this.sendMessagePacket(conversation.getAccount(), packet); + } + } + + public SecureRandom getRNG() { + return this.mRandom; + } + + public MemorizingTrustManager getMemorizingTrustManager() { + return this.mMemorizingTrustManager; + } + + public void setMemorizingTrustManager(MemorizingTrustManager trustManager) { + this.mMemorizingTrustManager = trustManager; + } + + public void updateMemorizingTrustmanager() { + final MemorizingTrustManager tm; + final boolean dontTrustSystemCAs = getBooleanPreference("dont_trust_system_cas", R.bool.dont_trust_system_cas); + if (dontTrustSystemCAs) { + tm = new MemorizingTrustManager(getApplicationContext(), null); + } else { + tm = new MemorizingTrustManager(getApplicationContext()); + } + setMemorizingTrustManager(tm); + } + + public LruCache getBitmapCache() { + return this.mBitmapCache; + } + + public Collection getKnownHosts() { + final Set hosts = new HashSet<>(); + for (final Account account : getAccounts()) { + hosts.add(account.getServer()); + for (final Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster()) { + final String server = contact.getServer(); + if (server != null) { + hosts.add(server); + } + } + } + } + if (Config.QUICKSY_DOMAIN != null) { + hosts.remove(Config.QUICKSY_DOMAIN); //we only want to show this when we type a e164 number + } + if (Config.DOMAIN_LOCK != null) { + hosts.add(Config.DOMAIN_LOCK); + } + if (Config.MAGIC_CREATE_DOMAIN != null) { + hosts.add(Config.MAGIC_CREATE_DOMAIN); + } + return hosts; + } + + public Collection getKnownConferenceHosts() { + final Set mucServers = new HashSet<>(); + for (final Account account : accounts) { + if (account.getXmppConnection() != null) { + mucServers.addAll(account.getXmppConnection().getMucServers()); + for (Bookmark bookmark : account.getBookmarks()) { + final Jid jid = bookmark.getJid(); + final String s = jid == null ? null : jid.getDomain(); + if (s != null) { + mucServers.add(s); + } + } + } + } + return mucServers; + } + + public void sendMessagePacket(Account account, MessagePacket packet) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendMessagePacket(packet); + } + } + + public void sendPresencePacket(Account account, PresencePacket packet) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendPresencePacket(packet); + } + } + + public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) { + final XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data); + connection.sendUnmodifiedIqPacket(request, connection.registrationResponseListener, true); + } + } + + public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) { + final XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendIqPacket(packet, callback); + } else if (callback != null) { + callback.onIqPacketReceived(account, new IqPacket(IqPacket.TYPE.TIMEOUT)); + } + } + + public void sendPresence(final Account account) { + sendPresence(account, checkListeners() && broadcastLastActivity()); + } + + private void sendPresence(final Account account, final boolean includeIdleTimestamp) { + Presence.Status status; + if (manuallyChangePresence()) { + status = account.getPresenceStatus(); + } else { + status = getTargetPresence(); + } + final PresencePacket packet = mPresenceGenerator.selfPresence(account, status); + if (mLastActivity > 0 && includeIdleTimestamp) { + long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates + packet.addChild("idle", Namespace.IDLE).setAttribute("since", AbstractGenerator.getTimestamp(since)); + } + sendPresencePacket(account, packet); + } + + private void deactivateGracePeriod() { + for (Account account : getAccounts()) { + account.deactivateGracePeriod(); + } + } + + public void refreshAllPresences() { + boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity(); + for (Account account : getAccounts()) { + if (account.isEnabled()) { + sendPresence(account, includeIdleTimestamp); + } + } + } + + private void refreshAllFcmTokens() { + for (Account account : getAccounts()) { + if (account.isOnlineAndConnected() && mPushManagementService.available(account)) { + mPushManagementService.registerPushTokenOnServer(account); + //TODO renew mucs + } + } + } + + private void sendOfflinePresence(final Account account) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence"); + sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); + } + + public MessageGenerator getMessageGenerator() { + return this.mMessageGenerator; + } + + public PresenceGenerator getPresenceGenerator() { + return this.mPresenceGenerator; + } + + public IqGenerator getIqGenerator() { + return this.mIqGenerator; + } + + public IqParser getIqParser() { + return this.mIqParser; + } + + public JingleConnectionManager getJingleConnectionManager() { + return this.mJingleConnectionManager; + } + + public MessageArchiveService getMessageArchiveService() { + return this.mMessageArchiveService; + } + + public QuickConversationsService getQuickConversationsService() { return this.mQuickConversationsService; } - public List findContacts(Jid jid, String accountJid) { - ArrayList contacts = new ArrayList<>(); - for (Account account : getAccounts()) { - if ((account.isEnabled() || accountJid != null) - && (accountJid == null || accountJid.equals(account.getJid().asBareJid().toString()))) { - Contact contact = account.getRoster().getContactFromContactList(jid); - if (contact != null) { - contacts.add(contact); - } - } - } - return contacts; - } + public List findContacts(Jid jid, String accountJid) { + ArrayList contacts = new ArrayList<>(); + for (Account account : getAccounts()) { + if ((account.isEnabled() || accountJid != null) + && (accountJid == null || accountJid.equals(account.getJid().asBareJid().toString()))) { + Contact contact = account.getRoster().getContactFromContactList(jid); + if (contact != null) { + contacts.add(contact); + } + } + } + return contacts; + } - public Conversation findFirstMuc(Jid jid) { - for (Conversation conversation : getConversations()) { - if (conversation.getAccount().isEnabled() && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) { - return conversation; - } - } - return null; - } + public Conversation findFirstMuc(Jid jid) { + for (Conversation conversation : getConversations()) { + if (conversation.getAccount().isEnabled() && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) { + return conversation; + } + } + return null; + } - public NotificationService getNotificationService() { - return this.mNotificationService; - } + public NotificationService getNotificationService() { + return this.mNotificationService; + } - public HttpConnectionManager getHttpConnectionManager() { - return this.mHttpConnectionManager; - } + public HttpConnectionManager getHttpConnectionManager() { + return this.mHttpConnectionManager; + } - public void resendFailedMessages(final Message message) { - final Collection messages = new ArrayList<>(); - Message current = message; - while (current.getStatus() == Message.STATUS_SEND_FAILED) { - messages.add(current); - if (current.mergeable(current.next())) { - current = current.next(); - } else { - break; - } - } - for (final Message msg : messages) { - msg.setTime(System.currentTimeMillis()); - markMessage(msg, Message.STATUS_WAITING); - this.resendMessage(msg, false); - } - if (message.getConversation() instanceof Conversation) { - ((Conversation) message.getConversation()).sort(); - } - updateConversationUi(); - } + public void resendFailedMessages(final Message message) { + final Collection messages = new ArrayList<>(); + Message current = message; + while (current.getStatus() == Message.STATUS_SEND_FAILED) { + messages.add(current); + if (current.mergeable(current.next())) { + current = current.next(); + } else { + break; + } + } + for (final Message msg : messages) { + msg.setTime(System.currentTimeMillis()); + markMessage(msg, Message.STATUS_WAITING); + this.resendMessage(msg, false); + } + if (message.getConversation() instanceof Conversation) { + ((Conversation) message.getConversation()).sort(); + } + updateConversationUi(); + } - public void clearConversationHistory(final Conversation conversation) { - final long clearDate; - final String reference; - if (conversation.countMessages() > 0) { - Message latestMessage = conversation.getLatestMessage(); - clearDate = latestMessage.getTimeSent() + 1000; - reference = latestMessage.getServerMsgId(); - } else { - clearDate = System.currentTimeMillis(); - reference = null; - } - conversation.clearMessages(); - conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam - conversation.setLastClearHistory(clearDate, reference); - Runnable runnable = () -> { - databaseBackend.deleteMessagesInConversation(conversation); - databaseBackend.updateConversation(conversation); - }; - mDatabaseWriterExecutor.execute(runnable); - } + public void clearConversationHistory(final Conversation conversation) { + final long clearDate; + final String reference; + if (conversation.countMessages() > 0) { + Message latestMessage = conversation.getLatestMessage(); + clearDate = latestMessage.getTimeSent() + 1000; + reference = latestMessage.getServerMsgId(); + } else { + clearDate = System.currentTimeMillis(); + reference = null; + } + conversation.clearMessages(); + conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam + conversation.setLastClearHistory(clearDate, reference); + Runnable runnable = () -> { + databaseBackend.deleteMessagesInConversation(conversation); + databaseBackend.updateConversation(conversation); + }; + mDatabaseWriterExecutor.execute(runnable); + } - public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) { - if (blockable != null && blockable.getBlockedJid() != null) { - final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), (a, response) -> { + public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) { + if (blockable != null && blockable.getBlockedJid() != null) { + final Jid jid = blockable.getBlockedJid(); + this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), (a, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { a.getBlocklist().add(jid); updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); } }); - if (blockable.getBlockedJid().isFullJid()) { - return false; + if (blockable.getBlockedJid().isFullJid()) { + return false; } else if (removeBlockedConversations(blockable.getAccount(), jid)) { - updateConversationUi(); - return true; - } else { - return false; - } - } else { - return false; - } - } + updateConversationUi(); + return true; + } else { + return false; + } + } else { + return false; + } + } - public boolean removeBlockedConversations(final Account account, final Jid blockedJid) { - boolean removed = false; - synchronized (this.conversations) { - boolean domainJid = blockedJid.getLocal() == null; - for (Conversation conversation : this.conversations) { - boolean jidMatches = (domainJid && blockedJid.getDomain().equals(conversation.getJid().getDomain())) - || blockedJid.equals(conversation.getJid().asBareJid()); - if (conversation.getAccount() == account - && conversation.getMode() == Conversation.MODE_SINGLE - && jidMatches) { - this.conversations.remove(conversation); - markRead(conversation); - conversation.setStatus(Conversation.STATUS_ARCHIVED); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conversation " + conversation.getJid().asBareJid() + " because jid was blocked"); - updateConversation(conversation); - removed = true; - } - } - } - return removed; - } + public boolean removeBlockedConversations(final Account account, final Jid blockedJid) { + boolean removed = false; + synchronized (this.conversations) { + boolean domainJid = blockedJid.getLocal() == null; + for (Conversation conversation : this.conversations) { + boolean jidMatches = (domainJid && blockedJid.getDomain().equals(conversation.getJid().getDomain())) + || blockedJid.equals(conversation.getJid().asBareJid()); + if (conversation.getAccount() == account + && conversation.getMode() == Conversation.MODE_SINGLE + && jidMatches) { + this.conversations.remove(conversation); + markRead(conversation); + conversation.setStatus(Conversation.STATUS_ARCHIVED); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conversation " + conversation.getJid().asBareJid() + " because jid was blocked"); + updateConversation(conversation); + removed = true; + } + } + } + return removed; + } - public void sendUnblockRequest(final Blockable blockable) { - if (blockable != null && blockable.getJid() != null) { - final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getBlocklist().remove(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - } - } - }); - } - } + public void sendUnblockRequest(final Blockable blockable) { + if (blockable != null && blockable.getJid() != null) { + final Jid jid = blockable.getBlockedJid(); + this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.getBlocklist().remove(jid); + updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); + } + } + }); + } + } - public void publishDisplayName(Account account) { - String displayName = account.getDisplayName(); - final IqPacket request; - if (TextUtils.isEmpty(displayName)) { + public void publishDisplayName(Account account) { + String displayName = account.getDisplayName(); + final IqPacket request; + if (TextUtils.isEmpty(displayName)) { request = mIqGenerator.deleteNode(Namespace.NICK); - } else { + } else { request = mIqGenerator.publishNick(displayName); } mAvatarService.clear(account); sendIqPacket(account, request, (account1, packet) -> { if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name "+packet.toString()); + Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet.toString()); } }); - } + } - public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair 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 ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair 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 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); - final String node = presence.getNode(); - final String ver = presence.getVer(); - final Element query = request.query("http://jabber.org/protocol/disco#info"); - if (node != null && ver != null) { - query.setAttribute("node",node+"#"+ver); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid); - sendIqPacket(account, request, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response); - if (presence.getVer().equals(discoveryResult.getVer())) { - databaseBackend.insertDiscoveryResult(discoveryResult); - injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult); - } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); - } - } - a.inProgressDiscoFetches.remove(key); - }); - } - } - } + public void fetchCaps(Account account, final Jid jid, final Presence presence) { + final Pair 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); + final String node = presence.getNode(); + final String ver = presence.getVer(); + final Element query = request.query("http://jabber.org/protocol/disco#info"); + if (node != null && ver != null) { + query.setAttribute("node", node + "#" + ver); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid); + sendIqPacket(account, request, (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response); + if (presence.getVer().equals(discoveryResult.getVer())) { + databaseBackend.insertDiscoveryResult(discoveryResult); + injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult); + } else { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); + } + } + a.inProgressDiscoFetches.remove(key); + }); + } + } + } - private void injectServiceDiscoveryResult(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); - } - } - } - } + private void injectServiceDiscoveryResult(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) { - final MessageArchiveService.Version version = MessageArchiveService.Version.get(account); - IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.addChild("prefs", version.namespace); - sendIqPacket(account, request, (account1, packet) -> { - Element prefs = packet.findChild("prefs", version.namespace); - if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) { - callback.onPreferencesFetched(prefs); - } else { - callback.onPreferencesFetchFailed(); - } - }); - } + public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) { + final MessageArchiveService.Version version = MessageArchiveService.Version.get(account); + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.addChild("prefs", version.namespace); + sendIqPacket(account, request, (account1, packet) -> { + Element prefs = packet.findChild("prefs", version.namespace); + if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) { + callback.onPreferencesFetched(prefs); + } else { + callback.onPreferencesFetchFailed(); + } + }); + } - public PushManagementService getPushManagementService() { - return mPushManagementService; - } + public PushManagementService getPushManagementService() { + return mPushManagementService; + } - public void changeStatus(Account account, PresenceTemplate template, String signature) { - if (!template.getStatusMessage().isEmpty()) { - databaseBackend.insertPresenceTemplate(template); - } - account.setPgpSignature(signature); - account.setPresenceStatus(template.getStatus()); - account.setPresenceStatusMessage(template.getStatusMessage()); - databaseBackend.updateAccount(account); - sendPresence(account); - } + public void changeStatus(Account account, PresenceTemplate template, String signature) { + if (!template.getStatusMessage().isEmpty()) { + databaseBackend.insertPresenceTemplate(template); + } + account.setPgpSignature(signature); + account.setPresenceStatus(template.getStatus()); + account.setPresenceStatusMessage(template.getStatusMessage()); + databaseBackend.updateAccount(account); + sendPresence(account); + } - public List getPresenceTemplates(Account account) { - List templates = databaseBackend.getPresenceTemplates(); - for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) { - if (!templates.contains(template)) { - templates.add(0, template); - } - } - return templates; - } + public List getPresenceTemplates(Account account) { + List templates = databaseBackend.getPresenceTemplates(); + for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) { + if (!templates.contains(template)) { + templates.add(0, template); + } + } + return templates; + } - public void saveConversationAsBookmark(Conversation conversation, String name) { - final Account account = conversation.getAccount(); - final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid()); - final String nick = conversation.getJid().getResource(); + public void saveConversationAsBookmark(Conversation conversation, String name) { + final Account account = conversation.getAccount(); + final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid()); + final String nick = conversation.getJid().getResource(); if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) { bookmark.setNick(nick); } - if (!TextUtils.isEmpty(name)) { - bookmark.setBookmarkName(name); - } - bookmark.setAutojoin(getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin))); - account.getBookmarks().add(bookmark); - pushBookmarks(account); - bookmark.setConversation(conversation); - } + if (!TextUtils.isEmpty(name)) { + bookmark.setBookmarkName(name); + } + bookmark.setAutojoin(getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin))); + createBookmark(account, bookmark); + bookmark.setConversation(conversation); + } - public boolean verifyFingerprints(Contact contact, List fingerprints) { - boolean performedVerification = false; - final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); - for (XmppUri.Fingerprint fp : fingerprints) { - if (fp.type == XmppUri.FingerprintType.OMEMO) { - String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); - FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint); - if (fingerprintStatus != null) { - if (!fingerprintStatus.isVerified()) { - performedVerification = true; - axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified()); - } - } else { - axolotlService.preVerifyFingerprint(contact, fingerprint); - } - } - } - return performedVerification; - } + public boolean verifyFingerprints(Contact contact, List fingerprints) { + boolean performedVerification = false; + final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); + for (XmppUri.Fingerprint fp : fingerprints) { + if (fp.type == XmppUri.FingerprintType.OMEMO) { + String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); + FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint); + if (fingerprintStatus != null) { + if (!fingerprintStatus.isVerified()) { + performedVerification = true; + axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified()); + } + } else { + axolotlService.preVerifyFingerprint(contact, fingerprint); + } + } + } + return performedVerification; + } - public boolean verifyFingerprints(Account account, List fingerprints) { - final AxolotlService axolotlService = account.getAxolotlService(); - boolean verifiedSomething = false; - for (XmppUri.Fingerprint fp : fingerprints) { - if (fp.type == XmppUri.FingerprintType.OMEMO) { - String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); - Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint); - FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint); - if (fingerprintStatus != null) { - if (!fingerprintStatus.isVerified()) { - axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified()); - verifiedSomething = true; - } - } else { - axolotlService.preVerifyFingerprint(account, fingerprint); - verifiedSomething = true; - } - } - } - return verifiedSomething; - } + public boolean verifyFingerprints(Account account, List fingerprints) { + final AxolotlService axolotlService = account.getAxolotlService(); + boolean verifiedSomething = false; + for (XmppUri.Fingerprint fp : fingerprints) { + if (fp.type == XmppUri.FingerprintType.OMEMO) { + String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); + Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint); + FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint); + if (fingerprintStatus != null) { + if (!fingerprintStatus.isVerified()) { + axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified()); + verifiedSomething = true; + } + } else { + axolotlService.preVerifyFingerprint(account, fingerprint); + verifiedSomething = true; + } + } + } + return verifiedSomething; + } - public boolean blindTrustBeforeVerification() { - return getBooleanPreference(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv); - } + public boolean blindTrustBeforeVerification() { + return getBooleanPreference(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv); + } - public ShortcutService getShortcutService() { - return mShortcutService; - } + public ShortcutService getShortcutService() { + return mShortcutService; + } - public void pushMamPreferences(Account account, Element prefs) { - IqPacket set = new IqPacket(IqPacket.TYPE.SET); - set.addChild(prefs); - sendIqPacket(account, set, null); - } + public void pushMamPreferences(Account account, Element prefs) { + IqPacket set = new IqPacket(IqPacket.TYPE.SET); + set.addChild(prefs); + sendIqPacket(account, set, null); + } - public interface OnMamPreferencesFetched { - void onPreferencesFetched(Element prefs); + public interface OnMamPreferencesFetched { + void onPreferencesFetched(Element prefs); - void onPreferencesFetchFailed(); - } + void onPreferencesFetchFailed(); + } - public interface OnAccountCreated { - void onAccountCreated(Account account); + public interface OnAccountCreated { + void onAccountCreated(Account account); - void informUser(int r); - } + void informUser(int r); + } - public interface OnMoreMessagesLoaded { - void onMoreMessagesLoaded(int count, Conversation conversation); + public interface OnMoreMessagesLoaded { + void onMoreMessagesLoaded(int count, Conversation conversation); - void informUser(int r); - } + void informUser(int r); + } - public interface OnAccountPasswordChanged { - void onPasswordChangeSucceeded(); + public interface OnAccountPasswordChanged { + void onPasswordChangeSucceeded(); - void onPasswordChangeFailed(); - } + void onPasswordChangeFailed(); + } public interface OnRoomDestroy { void onRoomDestroySucceeded(); @@ -4526,63 +4595,63 @@ public class XmppConnectionService extends Service { void onRoomDestroyFailed(); } - public interface OnAffiliationChanged { - void onAffiliationChangedSuccessful(Jid jid); + public interface OnAffiliationChanged { + void onAffiliationChangedSuccessful(Jid jid); - void onAffiliationChangeFailed(Jid jid, int resId); - } + void onAffiliationChangeFailed(Jid jid, int resId); + } - public interface OnConversationUpdate { - void onConversationUpdate(); - } + public interface OnConversationUpdate { + void onConversationUpdate(); + } - public interface OnAccountUpdate { - void onAccountUpdate(); - } + public interface OnAccountUpdate { + void onAccountUpdate(); + } - public interface OnCaptchaRequested { - void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha); - } + public interface OnCaptchaRequested { + void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha); + } - public interface OnRosterUpdate { - void onRosterUpdate(); - } + public interface OnRosterUpdate { + void onRosterUpdate(); + } - public interface OnMucRosterUpdate { - void onMucRosterUpdate(); - } + public interface OnMucRosterUpdate { + void onMucRosterUpdate(); + } - public interface OnConferenceConfigurationFetched { - void onConferenceConfigurationFetched(Conversation conversation); + public interface OnConferenceConfigurationFetched { + void onConferenceConfigurationFetched(Conversation conversation); - void onFetchFailed(Conversation conversation, Element error); - } + void onFetchFailed(Conversation conversation, Element error); + } - public interface OnConferenceJoined { - void onConferenceJoined(Conversation conversation); - } + public interface OnConferenceJoined { + void onConferenceJoined(Conversation conversation); + } - public interface OnConfigurationPushed { - void onPushSucceeded(); + public interface OnConfigurationPushed { + void onPushSucceeded(); - void onPushFailed(); - } + void onPushFailed(); + } - public interface OnShowErrorToast { - void onShowErrorToast(int resId); - } + public interface OnShowErrorToast { + void onShowErrorToast(int resId); + } - public class XmppConnectionBinder extends Binder { - public XmppConnectionService getService() { - return XmppConnectionService.this; - } - } + public class XmppConnectionBinder extends Binder { + public XmppConnectionService getService() { + return XmppConnectionService.this; + } + } - private class InternalEventReceiver extends BroadcastReceiver { + private class InternalEventReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - onStartCommand(intent,0,0); + onStartCommand(intent, 0, 0); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index 10bd4ced1..b1684473d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityChannelDiscoveryBinding; import eu.siacs.conversations.entities.Account; @@ -217,20 +218,21 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O return false; } - public void joinChannelSearchResult(String accountJid, MuclumbusService.Room result) { - final boolean syncAutojoin = getBooleanPreference("autojoin", R.bool.autojoin); - Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid)); + public void joinChannelSearchResult(String selectedAccount, MuclumbusService.Room result) { + final Jid jid = Config.DOMAIN_LOCK == null ? Jid.of(selectedAccount) : Jid.of(selectedAccount, Config.DOMAIN_LOCK, null); + final boolean syncAutoJoin = getBooleanPreference("autojoin", R.bool.autojoin); + final Account account = xmppConnectionService.findAccountByJid(jid); final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, result.getRoom(), true, true, true); - if (conversation.getBookmark() != null) { - if (!conversation.getBookmark().autojoin() && syncAutojoin) { - conversation.getBookmark().setAutojoin(true); - xmppConnectionService.pushBookmarks(account); + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + if (!bookmark.autojoin() && syncAutoJoin) { + bookmark.setAutojoin(true); + xmppConnectionService.createBookmark(account, bookmark); } } else { - final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid()); - bookmark.setAutojoin(syncAutojoin); - account.getBookmarks().add(bookmark); - xmppConnectionService.pushBookmarks(account); + bookmark = new Bookmark(account, conversation.getJid().asBareJid()); + bookmark.setAutojoin(syncAutoJoin); + xmppConnectionService.createBookmark(account, bookmark); } switchToConversation(conversation); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 863fd5780..755a6b045 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -386,11 +386,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } protected void deleteBookmark() { - Account account = mConversation.getAccount(); - Bookmark bookmark = mConversation.getBookmark(); - account.getBookmarks().remove(bookmark); + final Account account = mConversation.getAccount(); + final Bookmark bookmark = mConversation.getBookmark(); bookmark.setConversation(null); - xmppConnectionService.pushBookmarks(account); + xmppConnectionService.deleteBookmark(account, bookmark); updateView(); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 161e5bf42..c0ec6d790 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1044,6 +1044,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return; } + if (m.getStatus() == Message.STATUS_RECEIVED && t != null && (t.getStatus() == Transferable.STATUS_CANCELLED || t.getStatus() == Transferable.STATUS_FAILED)) { + return; + } + final boolean deleted = m.isDeleted(); final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED || m.getEncryption() == Message.ENCRYPTION_PGP; diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 9264d039e..55c619356 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -431,7 +431,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne bookmark.setConversation(conversation); if (!bookmark.autojoin() && getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin))) { bookmark.setAutojoin(true); - xmppConnectionService.pushBookmarks(bookmark.getAccount()); + xmppConnectionService.createBookmark(bookmark.getAccount(), bookmark); } SoftKeyboardUtils.hideSoftKeyboard(this); switchToConversation(conversation); @@ -478,9 +478,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_text, bookmark.getJid().toEscapedString())); builder.setPositiveButton(R.string.delete, (dialog, which) -> { bookmark.setConversation(null); - Account account = bookmark.getAccount(); - account.getBookmarks().remove(bookmark); - xmppConnectionService.pushBookmarks(account); + final Account account = bookmark.getAccount(); + xmppConnectionService.deleteBookmark(account, bookmark); filter(mSearchEditText.getText().toString()); }); builder.create().show(); @@ -1041,8 +1040,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) { bookmark.setNick(nick); } - account.getBookmarks().add(bookmark); - xmppConnectionService.pushBookmarks(account); + xmppConnectionService.createBookmark(account, bookmark); final Conversation conversation = xmppConnectionService .findOrCreateConversation(account, conferenceJid, true, true, true); bookmark.setConversation(conversation); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java index 405ddcb41..a2f423a04 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java @@ -1,17 +1,21 @@ package eu.siacs.conversations.ui.adapter; +import android.content.ActivityNotFoundException; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.databinding.DataBindingUtil; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.Toast; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -20,6 +24,7 @@ import java.util.concurrent.RejectedExecutionException; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.MediaPreviewBinding; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.Attachment; @@ -54,11 +59,24 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter { - int pos = mediaPreviews.indexOf(attachment); + final int pos = mediaPreviews.indexOf(attachment); mediaPreviews.remove(pos); notifyItemRemoved(pos); conversationFragment.toggleInputMethod(); }); + holder.binding.mediaPreview.setOnClickListener(v -> view(context, attachment)); + } + + private static void view(final Context context, Attachment attachment) { + final Intent view = new Intent(Intent.ACTION_VIEW); + final Uri uri = FileBackend.getUriForUri(context, attachment.getUri()); + view.setDataAndType(uri, attachment.getMime()); + view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + try { + context.startActivity(view); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); + } } public void addMediaPreviews(List attachments) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 2329a4262..0490a7ec6 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -187,7 +187,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie if (message.isFileOrImage() || transferable != null) { FileParams params = message.getFileParams(); filesize = params.size > 0 ? UIHelper.filesizeToString(params.size) : null; - if (transferable != null && transferable.getStatus() == Transferable.STATUS_FAILED) { + if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) { error = true; } } @@ -206,10 +206,6 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie info = getContext().getString(R.string.offering); break; case Message.STATUS_SEND_RECEIVED: - if (mIndicateReceived) { - viewHolder.indicatorReceived.setVisibility(View.VISIBLE); - } - break; case Message.STATUS_SEND_DISPLAYED: if (mIndicateReceived) { viewHolder.indicatorReceived.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 89dbd0a25..459be645b 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -53,11 +53,10 @@ public class ShareUtil { if (message.isGeoUri()) { shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); shareIntent.setType("text/plain"); - shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true); } else if (!message.isFileOrImage()) { shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString()); shareIntent.setType("text/plain"); - shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true); + shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, message.getStatus() == Message.STATUS_RECEIVED); } else { final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); try { diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java index 48e0c1d55..0ef6ef55e 100644 --- a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java +++ b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java @@ -14,23 +14,25 @@ public class SocksSocketFactory { private static final byte[] LOCALHOST = new byte[]{127,0,0,1}; - public static void createSocksConnection(Socket socket, String destination, int port) throws IOException { - InputStream proxyIs = socket.getInputStream(); - OutputStream proxyOs = socket.getOutputStream(); + public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException { + final InputStream proxyIs = socket.getInputStream(); + final OutputStream proxyOs = socket.getOutputStream(); proxyOs.write(new byte[]{0x05, 0x01, 0x00}); - byte[] response = new byte[2]; - proxyIs.read(response); - if (response[0] != 0x05 || response[1] != 0x00) { + proxyOs.flush(); + final byte[] handshake = new byte[2]; + proxyIs.read(handshake); + if (handshake[0] != 0x05 || handshake[1] != 0x00) { throw new SocksConnectionException("Socks 5 handshake failed"); } - byte[] dest = destination.getBytes(); - ByteBuffer request = ByteBuffer.allocate(7 + dest.length); + final byte[] dest = destination.getBytes(); + final ByteBuffer request = ByteBuffer.allocate(7 + dest.length); request.put(new byte[]{0x05, 0x01, 0x00, 0x03}); request.put((byte) dest.length); request.put(dest); request.putShort((short) port); proxyOs.write(request.array()); - response = new byte[7 + dest.length]; + proxyOs.flush(); + final byte[] response = new byte[7 + dest.length]; proxyIs.read(response); if (response[1] != 0x00) { if (response[1] == 0x04) { @@ -52,7 +54,7 @@ public class SocksSocketFactory { return false; } - public static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException { + private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException { Socket socket = new Socket(); try { socket.connect(address, Config.CONNECT_TIMEOUT * 1000); diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 84df63dc2..c99c1c1b2 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -274,6 +274,8 @@ public class UIHelper { getFileDescriptionString(context, message)), true); case Transferable.STATUS_FAILED: return new Pair<>(context.getString(R.string.file_transmission_failed), true); + case Transferable.STATUS_CANCELLED: + return new Pair<>(context.getString(R.string.file_transmission_cancelled), true); case Transferable.STATUS_UPLOADING: if (message.getStatus() == Message.STATUS_OFFERED) { return new Pair<>(context.getString(R.string.offering_x_file, diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 26a0b33a2..4be004b97 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -33,4 +33,6 @@ public final class Namespace { public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; public static final String MUC_USER = "http://jabber.org/protocol/muc#user"; + public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:0"; + public static final String BOOKMARKS2_COMPAT = BOOKMARKS2+"#compat"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 6ce01194f..213c6b5aa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1857,5 +1857,9 @@ public class XmppConnection implements Runnable { public boolean stanzaIds() { return hasDiscoFeature(account.getJid().asBareJid(), Namespace.STANZA_IDS); } + + public boolean bookmarks2() { + return Config.USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/; + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 919350ec8..ac88b52f0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -142,7 +142,7 @@ public class JingleConnection implements Transferable { @Override public void onFileTransferAborted() { - JingleConnection.this.sendCancel(); + JingleConnection.this.sendSessionTerminate("connectivity-error"); JingleConnection.this.fail(); } }; @@ -222,27 +222,32 @@ public class JingleConnection implements Transferable { return this.message.getCounterpart(); } - public void deliverPacket(JinglePacket packet) { - boolean returnResult = true; + void deliverPacket(JinglePacket packet) { if (packet.isAction("session-terminate")) { Reason reason = packet.getReason(); if (reason != null) { if (reason.hasChild("cancel")) { + this.cancelled = true; this.fail(); } else if (reason.hasChild("success")) { this.receiveSuccess(); } else { - this.fail(); + final List children = reason.getChildren(); + if (children.size() == 1) { + this.fail(children.get(0).getName()); + } else { + this.fail(); + } } } else { this.fail(); } } else if (packet.isAction("session-accept")) { - returnResult = receiveAccept(packet); + receiveAccept(packet); } else if (packet.isAction("session-info")) { - Element checksum = packet.getChecksum(); - Element file = checksum == null ? null : checksum.findChild("file"); - Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2"); + final Element checksum = packet.getChecksum(); + final Element file = checksum == null ? null : checksum.findChild("file"); + final Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2"); if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) { try { this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT); @@ -250,33 +255,44 @@ public class JingleConnection implements Transferable { this.expectedHash = new byte[0]; } } + respondToIq(packet, true); } else if (packet.isAction("transport-info")) { - returnResult = receiveTransportInfo(packet); + receiveTransportInfo(packet); } else if (packet.isAction("transport-replace")) { if (packet.getJingleContent().hasIbbTransport()) { - returnResult = this.receiveFallbackToIbb(packet); + receiveFallbackToIbb(packet); } else { - returnResult = false; - Log.d(Config.LOGTAG, "trying to fallback to something unknown" - + packet.toString()); + Log.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString()); + respondToIq(packet, false); } } else if (packet.isAction("transport-accept")) { - returnResult = this.receiveTransportAccept(packet); + receiveTransportAccept(packet); } else { - Log.d(Config.LOGTAG, "packet arrived in connection. action was " - + packet.getAction()); - returnResult = false; + Log.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction()); + respondToIq(packet, false); } - IqPacket response; - if (returnResult) { - response = packet.generateResponse(IqPacket.TYPE.RESULT); + } + private void respondToIq(final IqPacket packet, final boolean result) { + final IqPacket response; + if (result) { + response = packet.generateResponse(IqPacket.TYPE.RESULT); } else { response = packet.generateResponse(IqPacket.TYPE.ERROR); + final Element error = response.addChild("error").setAttribute("type", "cancel"); + error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas"); } mXmppConnectionService.sendIqPacket(account, response, null); } + private void respondToIqWithOutOfOrder(final IqPacket packet) { + final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); + final Element error = response.addChild("error").setAttribute("type", "wait"); + error.addChild("unexpected-request", "urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("out-of-order", "urn:xmpp:jingle:errors:1"); + mXmppConnectionService.sendIqPacket(account, response, null); + } + public void init(final Message message) { if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { Conversation conversation = (Conversation) message.getConversation(); @@ -320,7 +336,7 @@ public class JingleConnection implements Transferable { @Override public void failed() { - Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed"); + Log.d(Config.LOGTAG, String.format("connection to our own proxy65 candidate failed (%s:%d)", candidate.getHost(), candidate.getPort())); sendInitRequest(); } @@ -400,7 +416,6 @@ public class JingleConnection implements Transferable { this.contentName = content.getAttribute("name"); this.transportId = content.getTransportId(); - mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); if (this.initialTransport == Transport.SOCKS) { this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren())); @@ -411,20 +426,20 @@ public class JingleConnection implements Transferable { this.ibbBlockSize = Math.min(Integer.parseInt(receivedBlockSize), this.ibbBlockSize); } catch (NumberFormatException e) { Log.d(Config.LOGTAG, "number format exception " + e.getMessage()); - this.sendCancel(); + respondToIq(packet, false); this.fail(); return; } } else { Log.d(Config.LOGTAG, "received block size was null"); - this.sendCancel(); + respondToIq(packet, false); this.fail(); return; } } this.ftVersion = content.getVersion(); if (ftVersion == null) { - this.sendCancel(); + respondToIq(packet, false); this.fail(); return; } @@ -486,6 +501,9 @@ public class JingleConnection implements Transferable { //JET reports the plain text size. however lower levels of our receiving code still //expect the cipher text size. so we just + 16 bytes (auth tag size) here this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0)); + + respondToIq(packet, true); + if (mJingleConnectionManager.hasStoragePermission() && size < this.mJingleConnectionManager.getAutoAcceptFileSize() && mXmppConnectionService.isDataSaverDisabled()) { @@ -503,13 +521,9 @@ public class JingleConnection implements Transferable { this.mXmppConnectionService.getNotificationService().push(message); } Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize()); - } else { - this.sendCancel(); - this.fail(); + return; } - } else { - this.sendCancel(); - this.fail(); + respondToIq(packet, false); } } @@ -557,14 +571,17 @@ public class JingleConnection implements Transferable { try { this.mFileInputStream = new FileInputStream(file); } catch (FileNotFoundException e) { - abort(); + fail(e.getMessage()); return; } content.setTransportId(this.transportId); if (this.initialTransport == Transport.IBB) { content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize)); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending IBB offer"); } else { - content.socks5transport().setChildren(getCandidatesAsElements()); + final List candidates = getCandidatesAsElements(); + Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", account.getJid().asBareJid(), candidates.size())); + content.socks5transport().setChildren(candidates); } packet.setContent(content); this.sendJinglePacket(packet, (account, response) -> { @@ -682,18 +699,19 @@ public class JingleConnection implements Transferable { mXmppConnectionService.sendIqPacket(account, packet, callback); } - private boolean receiveAccept(JinglePacket packet) { + private void receiveAccept(JinglePacket packet) { if (this.mJingleStatus != JINGLE_STATUS_INITIATED) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order session-accept"); - return false; + respondToIqWithOutOfOrder(packet); + return; } this.mJingleStatus = JINGLE_STATUS_ACCEPTED; mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); Content content = packet.getJingleContent(); if (content.hasSocks5Transport()) { + respondToIq(packet, true); mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren())); this.connectNextCandidate(); - return true; } else if (content.hasIbbTransport()) { String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size"); if (receivedBlockSize != null) { @@ -706,18 +724,19 @@ public class JingleConnection implements Transferable { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in session-accept"); } } + respondToIq(packet, true); this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize); this.transport.connect(onIbbTransportConnected); - return true; } else { - return false; + respondToIq(packet, false); } } - private boolean receiveTransportInfo(JinglePacket packet) { - Content content = packet.getJingleContent(); + private void receiveTransportInfo(JinglePacket packet) { + final Content content = packet.getJingleContent(); if (content.hasSocks5Transport()) { if (content.socks5transport().hasChild("activated")) { + respondToIq(packet, true); if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) { onProxyActivated.success(); } else { @@ -729,21 +748,20 @@ public class JingleConnection implements Transferable { connection.setActivated(true); } else { Log.d(Config.LOGTAG, "activated connection not found"); - this.sendCancel(); + sendSessionTerminate("failed-transport"); this.fail(); } } - return true; } else if (content.socks5transport().hasChild("proxy-error")) { + respondToIq(packet, true); onProxyActivated.failed(); - return true; } else if (content.socks5transport().hasChild("candidate-error")) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received candidate error"); + respondToIq(packet, true); this.receivedCandidate = true; if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) { this.connect(); } - return true; } else if (content.socks5transport().hasChild("candidate-used")) { String cid = content.socks5transport().findChild("candidate-used").getAttribute("cid"); if (cid != null) { @@ -751,8 +769,10 @@ public class JingleConnection implements Transferable { JingleCandidate candidate = getCandidate(cid); if (candidate == null) { Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid); - return false; + respondToIq(packet, false); + return; } + respondToIq(packet, true); candidate.flagAsUsedByCounterpart(); this.receivedCandidate = true; if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) { @@ -760,15 +780,14 @@ public class JingleConnection implements Transferable { } else { Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate); } - return true; } else { - return false; + respondToIq(packet, false); } } else { - return false; + respondToIq(packet, false); } } else { - return true; + respondToIq(packet, true); } } @@ -867,11 +886,7 @@ public class JingleConnection implements Transferable { } private void sendSuccess() { - JinglePacket packet = bootstrapPacket("session-terminate"); - Reason reason = new Reason(); - reason.addChild("success"); - packet.setReason(reason); - this.sendJinglePacket(packet); + sendSessionTerminate("success"); this.disconnectSocks5Connections(); this.mJingleStatus = JINGLE_STATUS_FINISHED; this.message.setStatus(Message.STATUS_RECEIVED); @@ -893,7 +908,7 @@ public class JingleConnection implements Transferable { } - private boolean receiveFallbackToIbb(JinglePacket packet) { + private void receiveFallbackToIbb(JinglePacket packet) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receiving fallback to ibb"); final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size"); if (receivedBlockSize != null) { @@ -916,6 +931,7 @@ public class JingleConnection implements Transferable { content.ibbTransport().setAttribute("sid", this.transportId); answer.setContent(content); + respondToIq(packet, true); if (initiating()) { this.sendJinglePacket(answer, (account, response) -> { @@ -928,13 +944,13 @@ public class JingleConnection implements Transferable { this.transport.receive(file, onFileTransmissionStatusChanged); this.sendJinglePacket(answer); } - return true; } - private boolean receiveTransportAccept(JinglePacket packet) { + private void receiveTransportAccept(JinglePacket packet) { if (packet.getJingleContent().hasIbbTransport()) { - String receivedBlockSize = packet.getJingleContent().ibbTransport() - .getAttribute("block-size"); + final Element ibbTransport = packet.getJingleContent().ibbTransport(); + final String receivedBlockSize = ibbTransport.getAttribute("block-size"); + final String sid = ibbTransport.getAttribute("sid"); if (receivedBlockSize != null) { try { int bs = Integer.parseInt(receivedBlockSize); @@ -947,15 +963,19 @@ public class JingleConnection implements Transferable { } this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize); + if (sid == null || !sid.equals(this.transportId)) { + Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", account.getJid().asBareJid(), sid, transportId)); + } + respondToIq(packet, true); //might be receive instead if we are not initiating if (initiating()) { this.transport.connect(onIbbTransportConnected); } else { this.transport.receive(file, onFileTransmissionStatusChanged); } - return true; } else { - return false; + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invalid transport-accept"); + respondToIq(packet, false); } } @@ -977,18 +997,18 @@ public class JingleConnection implements Transferable { @Override public void cancel() { this.cancelled = true; - abort(); + abort("cancel"); } - public void abort() { + void abort(final String reason) { this.disconnectSocks5Connections(); if (this.transport instanceof JingleInbandTransport) { this.transport.disconnect(); } - this.sendCancel(); + sendSessionTerminate(reason); this.mJingleConnectionManager.finishConnection(this); if (responding()) { - this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED)); + this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED)); if (this.file != null) { file.delete(); } @@ -1013,7 +1033,7 @@ public class JingleConnection implements Transferable { FileBackend.close(mFileOutputStream); if (this.message != null) { if (responding()) { - this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED)); + this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED)); if (this.file != null) { file.delete(); } @@ -1028,11 +1048,11 @@ public class JingleConnection implements Transferable { this.mJingleConnectionManager.finishConnection(this); } - private void sendCancel() { - JinglePacket packet = bootstrapPacket("session-terminate"); - Reason reason = new Reason(); - reason.addChild("cancel"); - packet.setReason(reason); + private void sendSessionTerminate(String reason) { + final JinglePacket packet = bootstrapPacket("session-terminate"); + final Reason r = new Reason(); + r.addChild(reason); + packet.setReason(r); this.sendJinglePacket(packet); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 30057f532..f354c25af 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -106,7 +106,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { candidate.setPort(Integer.parseInt(port)); candidate.setType(JingleCandidate.TYPE_PROXY); candidate.setJid(proxy); - candidate.setPriority(655360 + (initiator ? 10 : 20)); + candidate.setPriority(655360 + (initiator ? 30 : 0)); primaryCandidates.put(account.getJid().asBareJid(),candidate); listener.onPrimaryCandidateFound(true,candidate); } catch (final NumberFormatException e) { @@ -166,7 +166,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void cancelInTransmission() { for (JingleConnection connection : this.connections) { if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) { - connection.abort(); + connection.abort("connectivity-error"); } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index 276040979..b4e5039aa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -23,223 +23,222 @@ import rocks.xmpp.addr.Jid; public class JingleInbandTransport extends JingleTransport { - private Account account; - private Jid counterpart; - private int blockSize; - private int seq = 0; - private String sessionId; + private Account account; + private Jid counterpart; + private int blockSize; + private int seq = 0; + private String sessionId; - private boolean established = false; + private boolean established = false; - private boolean connected = true; + private boolean connected = true; - private DownloadableFile file; - private JingleConnection connection; + private DownloadableFile file; + private JingleConnection connection; - private InputStream fileInputStream = null; - private InputStream innerInputStream = null; - private OutputStream fileOutputStream = null; - private long remainingSize = 0; - private long fileSize = 0; - private MessageDigest digest; + private InputStream fileInputStream = null; + private InputStream innerInputStream = null; + private OutputStream fileOutputStream = null; + private long remainingSize = 0; + private long fileSize = 0; + private MessageDigest digest; - private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged; + private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged; - private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (connected && packet.getType() == IqPacket.TYPE.RESULT) { - if (remainingSize > 0) { - sendNextBlock(); - } - } - } - }; + private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (!connected) { + return; + } + if (packet.getType() == IqPacket.TYPE.RESULT) { + if (remainingSize > 0) { + sendNextBlock(); + } + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + }; - public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) { - this.connection = connection; - this.account = connection.getAccount(); - this.counterpart = connection.getCounterPart(); - this.blockSize = blocksize; - this.sessionId = sid; - } - - private void sendClose() { - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.setTo(this.counterpart); - Element close = iq.addChild("close", "http://jabber.org/protocol/ibb"); - close.setAttribute("sid", this.sessionId); - this.account.getXmppConnection().sendIqPacket(iq, null); - } - - public void connect(final OnTransportConnected callback) { - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.setTo(this.counterpart); - Element open = iq.addChild("open", "http://jabber.org/protocol/ibb"); - open.setAttribute("sid", this.sessionId); - open.setAttribute("stanza", "iq"); - open.setAttribute("block-size", Integer.toString(this.blockSize)); - this.connected = true; - this.account.getXmppConnection().sendIqPacket(iq, - new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, - IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.RESULT) { - callback.failed(); - } else { - callback.established(); - } - } - }); - } - - @Override - public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) { - this.onFileTransmissionStatusChanged = callback; - this.file = file; - try { - this.digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - this.fileOutputStream = connection.getFileOutputStream(); - if (this.fileOutputStream == null) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": could not create output stream"); - callback.onFileTransferAborted(); - return; - } - this.remainingSize = this.fileSize = file.getExpectedSize(); - } catch (final NoSuchAlgorithmException | IOException e) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+" "+e.getMessage()); - callback.onFileTransferAborted(); - } + public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) { + this.connection = connection; + this.account = connection.getAccount(); + this.counterpart = connection.getCounterPart(); + this.blockSize = blocksize; + this.sessionId = sid; } - @Override - public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) { - this.onFileTransmissionStatusChanged = callback; - this.file = file; - try { - this.remainingSize = this.file.getExpectedSize(); - this.fileSize = this.remainingSize; - this.digest = MessageDigest.getInstance("SHA-1"); - this.digest.reset(); - fileInputStream = connection.getFileInputStream(); - if (fileInputStream == null) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": could no create input stream"); - callback.onFileTransferAborted(); - return; - } - innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream); - if (this.connected) { - this.sendNextBlock(); - } - } catch (Exception e) { - callback.onFileTransferAborted(); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": "+e.getMessage()); - } - } + private void sendClose() { + IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + iq.setTo(this.counterpart); + Element close = iq.addChild("close", "http://jabber.org/protocol/ibb"); + close.setAttribute("sid", this.sessionId); + this.account.getXmppConnection().sendIqPacket(iq, null); + } - @Override - public void disconnect() { - this.connected = false; - FileBackend.close(fileOutputStream); - FileBackend.close(fileInputStream); - } + public void connect(final OnTransportConnected callback) { + IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + iq.setTo(this.counterpart); + Element open = iq.addChild("open", "http://jabber.org/protocol/ibb"); + open.setAttribute("sid", this.sessionId); + open.setAttribute("stanza", "iq"); + open.setAttribute("block-size", Integer.toString(this.blockSize)); + this.connected = true; + this.account.getXmppConnection().sendIqPacket(iq, (account, packet) -> { + if (packet.getType() != IqPacket.TYPE.RESULT) { + callback.failed(); + } else { + callback.established(); + } + }); + } - private void sendNextBlock() { - byte[] buffer = new byte[this.blockSize]; - try { - int count = innerInputStream.read(buffer); - if (count == -1) { - sendClose(); - file.setSha1Sum(digest.digest()); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": sendNextBlock() count was -1"); - this.onFileTransmissionStatusChanged.onFileTransmitted(file); - fileInputStream.close(); - return; - } else if (count != buffer.length) { - int rem = innerInputStream.read(buffer,count,buffer.length-count); - if (rem > 0) { - count += rem; - } - } - this.remainingSize -= count; - this.digest.update(buffer,0,count); - String base64 = Base64.encodeToString(buffer,0,count, Base64.NO_WRAP); - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.setTo(this.counterpart); - Element data = iq.addChild("data", "http://jabber.org/protocol/ibb"); - data.setAttribute("seq", Integer.toString(this.seq)); - data.setAttribute("block-size", Integer.toString(this.blockSize)); - data.setAttribute("sid", this.sessionId); - data.setContent(base64); - this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived); - this.account.getXmppConnection().r(); //don't fill up stanza queue too much - this.seq++; - connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); - if (this.remainingSize <= 0) { - sendClose(); - file.setSha1Sum(digest.digest()); - this.onFileTransmissionStatusChanged.onFileTransmitted(file); - fileInputStream.close(); - } - } catch (IOException e) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": io exception during sendNextBlock() "+e.getMessage()); - FileBackend.close(fileInputStream); - this.onFileTransmissionStatusChanged.onFileTransferAborted(); - } - } + @Override + public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + this.fileOutputStream = connection.getFileOutputStream(); + if (this.fileOutputStream == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not create output stream"); + callback.onFileTransferAborted(); + return; + } + this.remainingSize = this.fileSize = file.getExpectedSize(); + } catch (final NoSuchAlgorithmException | IOException e) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " " + e.getMessage()); + callback.onFileTransferAborted(); + } + } - private void receiveNextBlock(String data) { - try { - byte[] buffer = Base64.decode(data, Base64.NO_WRAP); - if (this.remainingSize < buffer.length) { - buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize); - } - this.remainingSize -= buffer.length; - this.fileOutputStream.write(buffer); - this.digest.update(buffer); - if (this.remainingSize <= 0) { - file.setSha1Sum(digest.digest()); - fileOutputStream.flush(); - fileOutputStream.close(); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": receive next block nothing remaining"); - this.onFileTransmissionStatusChanged.onFileTransmitted(file); - } else { - connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); - } - } catch (Exception e) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": "+e.getMessage()); - FileBackend.close(fileOutputStream); - this.onFileTransmissionStatusChanged.onFileTransferAborted(); - } - } + @Override + public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.remainingSize = this.file.getExpectedSize(); + this.fileSize = this.remainingSize; + this.digest = MessageDigest.getInstance("SHA-1"); + this.digest.reset(); + fileInputStream = connection.getFileInputStream(); + if (fileInputStream == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could no create input stream"); + callback.onFileTransferAborted(); + return; + } + innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream); + if (this.connected) { + this.sendNextBlock(); + } + } catch (Exception e) { + callback.onFileTransferAborted(); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage()); + } + } - public void deliverPayload(IqPacket packet, Element payload) { - if (payload.getName().equals("open")) { - if (!established) { - established = true; - connected = true; - this.receiveNextBlock(""); - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); - } else { - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.ERROR), null); - } - } else if (connected && payload.getName().equals("data")) { - this.receiveNextBlock(payload.getContent()); - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); - } else if (connected && payload.getName().equals("close")) { - this.connected = false; - this.account.getXmppConnection().sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received ibb close"); - } else { - Log.d(Config.LOGTAG,payload.toString()); - // TODO some sort of exception - } - } + @Override + public void disconnect() { + this.connected = false; + FileBackend.close(fileOutputStream); + FileBackend.close(fileInputStream); + } + + private void sendNextBlock() { + byte[] buffer = new byte[this.blockSize]; + try { + int count = innerInputStream.read(buffer); + if (count == -1) { + sendClose(); + file.setSha1Sum(digest.digest()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sendNextBlock() count was -1"); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + fileInputStream.close(); + return; + } else if (count != buffer.length) { + int rem = innerInputStream.read(buffer, count, buffer.length - count); + if (rem > 0) { + count += rem; + } + } + this.remainingSize -= count; + this.digest.update(buffer, 0, count); + String base64 = Base64.encodeToString(buffer, 0, count, Base64.NO_WRAP); + IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + iq.setTo(this.counterpart); + Element data = iq.addChild("data", "http://jabber.org/protocol/ibb"); + data.setAttribute("seq", Integer.toString(this.seq)); + data.setAttribute("block-size", Integer.toString(this.blockSize)); + data.setAttribute("sid", this.sessionId); + data.setContent(base64); + this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived); + this.account.getXmppConnection().r(); //don't fill up stanza queue too much + this.seq++; + connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); + if (this.remainingSize <= 0) { + sendClose(); + file.setSha1Sum(digest.digest()); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + fileInputStream.close(); + } + } catch (IOException e) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during sendNextBlock() " + e.getMessage()); + FileBackend.close(fileInputStream); + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + private void receiveNextBlock(String data) { + try { + byte[] buffer = Base64.decode(data, Base64.NO_WRAP); + if (this.remainingSize < buffer.length) { + buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize); + } + this.remainingSize -= buffer.length; + this.fileOutputStream.write(buffer); + this.digest.update(buffer); + if (this.remainingSize <= 0) { + file.setSha1Sum(digest.digest()); + fileOutputStream.flush(); + fileOutputStream.close(); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receive next block nothing remaining"); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + } else { + connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); + } + } catch (Exception e) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage()); + FileBackend.close(fileOutputStream); + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + public void deliverPayload(IqPacket packet, Element payload) { + if (payload.getName().equals("open")) { + if (!established) { + established = true; + connected = true; + this.receiveNextBlock(""); + this.account.getXmppConnection().sendIqPacket( + packet.generateResponse(IqPacket.TYPE.RESULT), null); + } else { + this.account.getXmppConnection().sendIqPacket( + packet.generateResponse(IqPacket.TYPE.ERROR), null); + } + } else if (connected && payload.getName().equals("data")) { + this.receiveNextBlock(payload.getContent()); + this.account.getXmppConnection().sendIqPacket( + packet.generateResponse(IqPacket.TYPE.RESULT), null); + } else if (connected && payload.getName().equals("close")) { + this.connected = false; + this.account.getXmppConnection().sendIqPacket( + packet.generateResponse(IqPacket.TYPE.RESULT), null); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close"); + } else { + Log.d(Config.LOGTAG, payload.toString()); + // TODO some sort of exception + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index be72d9327..e6b23ad18 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -26,6 +26,10 @@ import eu.siacs.conversations.utils.WakeLockHelper; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; public class JingleSocks5Transport extends JingleTransport { + + private static final int SOCKET_TIMEOUT_DIRECT = 3000; + private static final int SOCKET_TIMEOUT_PROXY = 5000; + private final JingleCandidate candidate; private final JingleConnection connection; private final String destination; @@ -92,8 +96,9 @@ public class JingleSocks5Transport extends JingleTransport { } } - private void acceptIncomingSocketConnection(Socket socket) throws IOException { + private void acceptIncomingSocketConnection(final Socket socket) throws IOException { Log.d(Config.LOGTAG, "accepted connection from " + socket.getInetAddress().getHostAddress()); + socket.setSoTimeout(SOCKET_TIMEOUT_DIRECT); final byte[] authBegin = new byte[2]; final InputStream inputStream = socket.getInputStream(); final OutputStream outputStream = socket.getOutputStream(); @@ -115,7 +120,8 @@ public class JingleSocks5Transport extends JingleTransport { int destinationCount = inputStream.read(); final byte[] destination = new byte[destinationCount]; inputStream.read(destination); - final int port = inputStream.read(); + final byte[] port = new byte[2]; + inputStream.read(port); final String receivedDestination = new String(destination); final ByteBuffer response = ByteBuffer.allocate(7 + destination.length); final byte[] responseHeader; @@ -131,11 +137,12 @@ public class JingleSocks5Transport extends JingleTransport { response.put(responseHeader); response.put((byte) destination.length); response.put(destination); - response.putShort((short) port); + response.put(port); outputStream.write(response.array()); outputStream.flush(); if (success) { Log.d(Config.LOGTAG,connection.getAccount().getJid().asBareJid()+": successfully processed connection to candidate "+candidate.getHost()+":"+candidate.getPort()); + socket.setSoTimeout(0); this.socket = socket; this.inputStream = inputStream; this.outputStream = outputStream; @@ -151,6 +158,7 @@ public class JingleSocks5Transport extends JingleTransport { public void connect(final OnTransportConnected callback) { new Thread(() -> { + final int timeout = candidate.getType() == JingleCandidate.TYPE_DIRECT ? SOCKET_TIMEOUT_DIRECT : SOCKET_TIMEOUT_PROXY; try { final boolean useTor = connection.getAccount().isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect(); if (useTor) { @@ -158,11 +166,11 @@ public class JingleSocks5Transport extends JingleTransport { } else { socket = new Socket(); SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort()); - socket.connect(address, 5000); + socket.connect(address, timeout); } inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); - socket.setSoTimeout(5000); + socket.setSoTimeout(timeout); SocksSocketFactory.createSocksConnection(socket, destination, 0); socket.setSoTimeout(0); isEstablished = true; diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java index f6bfd451e..62f65ee1f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java @@ -14,14 +14,27 @@ public class PublishOptions { public static Bundle openAccess() { final Bundle options = new Bundle(); - options.putString("pubsub#access_model","open"); + options.putString("pubsub#access_model", "open"); return options; } public static Bundle persistentWhitelistAccess() { final Bundle options = new Bundle(); - options.putString("pubsub#persist_items","true"); - options.putString("pubsub#access_model","whitelist"); + options.putString("pubsub#persist_items", "true"); + options.putString("pubsub#access_model", "whitelist"); + return options; + } + + public static Bundle persistentWhitelistAccessMaxItems() { + final Bundle options = new Bundle(); + options.putString("pubsub#persist_items", "true"); + options.putString("pubsub#access_model", "whitelist"); + options.putString("pubsub#send_last_published_item", "never"); + options.putString("pubsub#max_items", "128"); //YOLO! + + options.putString("pubsub#notify_delete", "true"); + options.putString("pubsub#notify_retract", "true"); //one could also set notify=true on the retract + return options; } diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 895707338..7abde5760 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -337,6 +337,7 @@ %s zum Herunterladen angeboten Übertragung abbrechen Übertragung fehlgeschlagen + Übertragung abgebrochen Datei wurde gelöscht Keine Anwendung zum Öffnen der Datei gefunden Keine Anwendung zum Öffnen des Links gefunden @@ -857,7 +858,7 @@ Channels entdecken Channels suchen Mögliche Datenschutzverletzung! - search.jabbercat.org.

Wenn du diese Funktion verwendest, werden deine IP-Adresse und deine Suchbegriffe an diesen Dienst übertragen. Weitere Informationen findest du in der Datenschutzerklärung.]]>
+ search.jabber.network.

Wenn du diese Funktion verwendest, werden deine IP-Adresse und deine Suchbegriffe an diesen Dienst übertragen. Weitere Informationen findest du in der Datenschutzerklärung.]]>
Ich habe bereits ein Konto Vorhandenes Konto hinzufügen Neues Konto erstellen diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 07e002163..121738426 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -857,7 +857,6 @@ Ανακάλυψη καναλιών Αναζήτηση καναλιών Πιθανή παραβίαση ιδιωτικότητας! - search.jabbercat.org.

Χρησιμοποιώντας αυτή τη λειτουργία θα μεταβιβαστεί η διεύθυνση IP σας και οι όροι αναζήτησης σε αυτή την υπηρεσία. Δείτε την Πολιτική Ιδιωτικότητας της για περισσότερες πληροφορίες.]]>
Έχω ήδη λογαριασμό Προσθήκη υπάρχοντος λογαριασμού Εγγραφή νέου λογαριασμού diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 3c63859d9..01f649d2b 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -857,7 +857,6 @@ Descubrir canales Buscar canales ¡Posible violación de privacidad! - .

Usando esta funcionalidad transmitirás tu dirección IP y los términos buscados a este servicio. Ver su Política de Privacidad para más información.]]>
Ya tengo una cuenta Añadir una cuenta existente Registrar una cuenta nueva @@ -872,4 +871,5 @@ Esta cuenta ya fue configurada Por favor ingrese la contraseña para esta cuenta No se ha podido realizar esta acción + Unirse a canal público... diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index ab15680fb..bc769c730 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -30,6 +30,7 @@ orain min 1 lehenago %d min lehenago + Irakurrik gabeko %d elkarrizketa bidaltzen… Mezua desenkriptatzen. Mesedez itxaron… OpenPGPz enkriptatutako mezua @@ -336,6 +337,7 @@ %s deskargatzeko eskeinita Transmisioa utzi fitxategi transmisioak huts egin du + fitxategiaren transmisioa utzi egin da Fitxategia ezabatu egin da Fitxategia ireki dezakeen aplikaziorik ez da aurkitu Ez da lotura hau ireki dezakeen aplikaziorik aurkitu @@ -854,7 +856,7 @@ Kanalak aurkitu Kanalak bilatu Balizko pribatutasun urraketa! - search.jabbercat.org izeneko hirugarren zerbitzu bat erabiltzen du.

Ezaugarri hau erabiltzeak zure IP helbidea eta bilatutako testua zerbitzu horretara bidaltzea dakar. Ikusi beren pribatutasun politika informazio gehiago lortzeko.]]>
+ search.jabber.network. izeneko hirugarren zerbitzu bat erabiltzen du.

Ezaugarri hau erabiltzeak zure IP helbidea eta bilatutako testua zerbitzu horretara bidaltzea dakar. Ikusi beren pribatutasun politika informazio gehiago lortzeko.]]>
Badaukat kontu bat dagoeneko Gehitu existitzen den kontu bat Kontu berria erregistratu @@ -868,4 +870,6 @@ Hautatu duzun fitxategia ez da Conversations babes-kopia bat Kontu hau konfiguratuta dago jada Mesedez idatzi ezazu kontu honetarako pasahitza + Ezin izan da ekintza hau burutu + Kanal publiko batean sartu… diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index c19899d79..d0cff7724 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -7,7 +7,7 @@ Fermer cette conversation Détails du contact Détails de la conversation de groupe - Détails de la chaîne + Détails du canal Conversation sécurisée Ajouter un compte Modifier le nom @@ -17,6 +17,8 @@ Débloquer le contact Bloquer le domaine Débloquer le domaine + Bloquer le participant + Débloquer le participant Gestion des comptes Paramètres Partager avec Conversation @@ -28,6 +30,7 @@ À l\'instant Il y a 1 minute Il y a %d minutes + %d conversations non lues Envoi… Déchiffrement du message. Veuillez patienter... Message chiffré avec OpenPGP @@ -334,6 +337,7 @@ %s proposé à télécharger Annuler l\'envoi Échec de l\'envoi du fichier + Transfert de fichier annulé Le fichier a été supprimé Aucune application disponible pour ouvrir le fichier Aucune application trouvée pour ouvrir le lien @@ -701,7 +705,7 @@ Petite Moyenne Grande - OMEMO sera utilisé par défaut pour toute nouvelle conversation. + Le message n\'était pas chiffré pour cet appareil. Échec de déchiffrement du message OMEMO. annuler Le partage de positionnement est désactivé. @@ -845,7 +849,7 @@ N\'importe qui peut inviter d\'autres personnes. Les adresses XMPP sont visibles par les administrateurs. Les adresses XMPP sont visibles par tous. - Cette chaîne publique n\'a pas de participants. Invitez vos contacts ou utilisez le bouton de partage pour distribuer son adresse XMPP. + Ce canal publique n\'a pas de participants. Invitez vos contacts ou utilisez le bouton de partage pour distribuer son adresse XMPP. Ce chat de groupe privé n\'a aucun participant. Gérer les privilèges Rechercher des participants @@ -854,11 +858,20 @@ Découverte des canaux Recherche des canaux Violation possible de la confidentialité ! - search.jabbercat.org qui transmet votre adresse IP et les termes de recherche à ce service. Voir leur politique de confidentialité pour plus d\'informations]]>. + search.jabber.network.

L\'utilisation de cette fonction transmettra votre adresse IP et les termes de recherche à ce service. Veuillez consulter leur Politique de confidentialité pour plus d\'information.]]>
J\'ai déjà un compte Ajouter un compte existant Enregistrer un nouveau compte Ceci ressemble à une adresse de domaine Ajouter quand même Ceci ressemble à une adresse de canal + Partager les fichiers de sauvegardes + Sauvegarder les conversations + Événement + Ouvrir sauvegarde + Le fichier sélectionné n\'est pas une sauvegarde de Conversations + Ce compte a déjà été configuré + Veuillez saisir le mot de passe pour ce compte + Action impossible à réaliser + Rejoindre le canal public ... diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index d9b407b0f..bf4088dfe 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -857,7 +857,6 @@ Descubrir canales Buscar canales Posible intrusión na intimidade! - search.jabbercat.org.

Ao utilizar esta característica transmitirá o seu enderezo IP e os termos de busca a ese servizo. Lea a súa Política de Intimidade para máis información.]]>
Xa teño unha conta Engadir conta existente Rexistrar unha nova conta diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index 2a0ecfe3c..239bb7064 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -856,7 +856,6 @@ Csatornák felderítése Csatornák keresése Magánélet lehetséges megsértése! - search.jabbercat.org.

Ezen funkció használata során át fog kerülni az IP címe és a keresési kifejezés ahhoz a szolgáltatáshoz. További információért tekintse meg az Adatvédelmi Irányelveiket.]]>
Már rendelkezem fiókkal Már létező fiók hozzáadása Új fiók létrehozása diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 2db97386c..44a5dc410 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -857,7 +857,6 @@ Individua i canali Cerca i canali Possibile violazione della privacy! - search.jabbercat.org.

Usando questa opzione trasmetterai il tuo indirizzo IP e la stringa di ricerca al servizio. Controlla la loro Policy per la privacy per maggiori informazioni.]]>
Ho già un account Aggiungi un account pre-esistente Registra un nuovo account diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 42572df40..87be4330c 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -337,6 +337,7 @@ %s aangeboden om te downloaden Bestandsoverdracht annuleren bestandsoverdracht mislukt + bestandsoverdracht geannuleerd Het bestand is verwijderd Geen applicatie om bestand te openen Geen applicatie om verwijzing te openen @@ -856,7 +857,7 @@ Kanalen ontdekken Kanalen doorzoeken Mogelijke privacyschending! - search.jabbercat.org.

Door deze functie te gebruiken, zullen je IP-adres en zoekopdrachten naar die dienst verstuurd worden. Bekijk hun privacybeleid voor meer informatie.]]>
+ search.jabber.network.

Door deze functie te gebruiken, zullen je IP-adres en zoekopdrachten naar die dienst verstuurd worden. Bekijk hun privacybeleid voor meer informatie.]]>
Ik heb al een account Bestaande account toevoegen Nieuwe account registreren @@ -871,4 +872,5 @@ Deze account is al ingesteld Voer het wachtwoord voor deze account in Kan deze actie niet uitvoeren + Deelnemen aan openbaar kanaal… diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 580a01817..442a4b742 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -874,7 +874,6 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Odkryj kanały Wyszukaj kanał Możliwe naruszenie prywatności! - search.jabbercat.org.

Używając tej funkcji twój adres IP oraz kryteria wyszukiwania zostaną wysłane do tej usługi. Sprawdź Politykę Prywatności aby uzyskać więcej informacji.]]>
Już mam konto Dodaj istniejące konto Zarejestruj nowe konto diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 4633f0fdf..0d7b98eeb 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -856,7 +856,6 @@ Descobrir canais Pesquisar canais Provável violação de privacidade! - search.jabbercat.org.

Ao usar esse recurso, você enviará o seu endereço IP e termos de pesquisa para esse serviço. Veja sua Política de Privacidade para maiores informações.]]>
Eu já tenho uma conta. Adicionar uma conta já existente Registrar uma nova conta diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index f341c656e..2e8104252 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -337,6 +337,7 @@ %s - fișier oferit spre descărcare Anulează transmisiunea transmisie fișier eșuată + transmisia fișierului a fost anulată Fișierul a fost șters Nu s-a găsit nici o aplicație care să deschidă fișierul Nu s-a găsit nici o aplicație care să deschidă legătura @@ -866,7 +867,7 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Descoperă canale publice Caută canale publice Posibilă încălcare a intimității! - search.jabbercat.org.

Folosind această funcție se va transmite adresa dumneavoastră IP și cuvintele căutate către acest serviciu. Pentru mai multe informații citiți Politica de confidențialitate a serviciului.]]>
+ search.jabber.network.

Folosind această funcție se va transmite adresa dumneavoastră IP și cuvintele căutate către acest serviciu. Pentru mai multe informații citiți Politica de confidențialitate a serviciului.]]>
Eu am deja un cont Adaugă un cont existent Înregistrează un cont nou diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 216a8fc4e..d631e8ffe 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -871,7 +871,6 @@ Знайти канали Шукати канали Можливе порушення приватності! - search.jabbercat.org.

Використання цієї функції передає Вашу IP адресу та пошукові запити цьому сервісу. Перегляньте їхню політику конфіденційності, щоб отримати більше інформації.]]>
Я вже маю обліковий запис Додати наявний обліковий запис Зареєструвати новий обліковий запис diff --git a/src/main/res/values-v21/themes.xml b/src/main/res/values-v21/themes.xml deleted file mode 100644 index d559dcbe7..000000000 --- a/src/main/res/values-v21/themes.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 9995d90c4..7a090cb65 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -850,7 +850,6 @@ 发现群聊 搜索群聊 可能侵犯隐私! - search.jabbercat.org的第三方服务。在探索群聊时,您的IP地址和搜索内容将传送到他们的服务器上。有关更多信息,请参阅他们的隐私政策。]]> 我已有账户 添加已有账户 注册新账户 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 43e903332..0a9452ecd 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -337,6 +337,7 @@ %s offered for download Cancel transmission file transmission failed + file transmission cancelled The file has been deleted No application found to open file No application found to open link @@ -859,7 +860,7 @@ Discover channels Search channels Possible privacy violation! - search.jabbercat.org.

Using this feature will transmit your IP address and search terms to that service. See their Privacy Policy for more information.]]>
+ search.jabber.network.

Using this feature will transmit your IP address and search terms to that service. See their Privacy Policy for more information.]]>
I already have an account Add existing account Register new account diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 60e0452c1..385458c61 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -1,5 +1,5 @@ - + -