diff --git a/CHANGELOG.md b/CHANGELOG.md index 4224b4ed5..42fe078ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +### Version 2.5.4 +* stability improvements for group chats and channels + ### Version 2.5.3 * bug fixes for peer to peer file transfer (Jingle) * fixed server info for unlimited/unknown max file size diff --git a/build.gradle b/build.gradle index 81ebd74b3..3abe985b5 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ dependencies { conversationsFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion" quicksyFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion" implementation 'org.bouncycastle:bcmail-jdk15on:1.58' - implementation 'com.google.zxing:core:3.3.3' + implementation 'com.google.zxing:core:3.4.0' implementation 'de.measite.minidns:minidns-hla:0.2.4' implementation 'me.leolin:ShortcutBadger:1.1.22@aar' implementation 'org.whispersystems:signal-protocol-java:2.6.2' @@ -59,13 +59,13 @@ dependencies { implementation "com.wefika:flowlayout:0.4.1" implementation 'net.ypresto.androidtranscoder:android-transcoder:0.3.0' implementation project(':libs:xmpp-addr') - implementation 'org.osmdroid:osmdroid-android:6.0.3' + implementation 'org.osmdroid:osmdroid-android:6.1.0' implementation 'org.hsluv:hsluv:0.2' - implementation 'org.conscrypt:conscrypt-android:1.3.0' + implementation 'org.conscrypt:conscrypt-android:2.1.0' implementation 'me.drakeet.support:toastcompat:1.1.0' implementation "com.leinardi.android:speed-dial:2.0.1" - implementation 'com.squareup.retrofit2:retrofit:2.5.0' - implementation 'com.squareup.retrofit2:converter-gson:2.5.0' + implementation 'com.squareup.retrofit2:retrofit:2.6.0' + implementation 'com.squareup.retrofit2:converter-gson:2.6.0' implementation 'com.google.guava:guava:27.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.10.1' } @@ -81,8 +81,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 25 - versionCode 330 - versionName "2.5.3" + versionCode 333 + versionName "2.5.4" archivesBaseName += "-$versionName" applicationId "eu.sum7.conversations" resValue "string", "applicationId", applicationId diff --git a/proguard-rules.pro b/proguard-rules.pro index dd566d10c..48bbfecbe 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -20,3 +20,34 @@ -dontwarn com.google.firebase.analytics.connector.AnalyticsConnector -dontwarn java.lang.** -dontwarn javax.lang.** + + +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod + +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations + +# Retain service method parameters when optimizing. +-keepclassmembers,allowshrinking,allowobfuscation interface * { + @retrofit2.http.* ; +} + +# Ignore annotation used for build tooling. +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions$* + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> diff --git a/src/conversations/res/values-ru/strings.xml b/src/conversations/res/values-ru/strings.xml new file mode 100644 index 000000000..338453d3d --- /dev/null +++ b/src/conversations/res/values-ru/strings.xml @@ -0,0 +1,6 @@ + + + Выберите своего XMPP-провайдера + Использовать conversations.im + Создать новый аккаунт + \ No newline at end of file diff --git a/src/free/java/eu/siacs/conversations/services/PushManagementService.java b/src/free/java/eu/siacs/conversations/services/PushManagementService.java index ffce90b8a..9fac3655e 100644 --- a/src/free/java/eu/siacs/conversations/services/PushManagementService.java +++ b/src/free/java/eu/siacs/conversations/services/PushManagementService.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.services; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; public class PushManagementService { @@ -10,7 +11,19 @@ public class PushManagementService { this.mXmppConnectionService = service; } - public void registerPushTokenOnServer(Account account) { + void registerPushTokenOnServer(Account account) { + //stub implementation. only affects playstore flavor + } + + void registerPushTokenOnServer(Conversation conversation) { + //stub implementation. only affects playstore flavor + } + + void unregisterChannel(Account account, String hash) { + //stub implementation. only affects playstore flavor + } + + void disablePushOnServer(Conversation conversation) { //stub implementation. only affects playstore flavor } diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 35528ab2c..69f78c913 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.crypto; import android.app.PendingIntent; import android.content.Intent; -import android.os.Parcelable; import android.support.annotation.StringRes; import android.util.Log; @@ -94,7 +93,7 @@ public class PgpEngine { break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); + callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); break; case OpenPgpApi.RESULT_CODE_ERROR: OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); @@ -133,7 +132,7 @@ public class PgpEngine { callback.success(message); break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); + callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); break; case OpenPgpApi.RESULT_CODE_ERROR: logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); @@ -200,7 +199,7 @@ public class PgpEngine { callback.success(account); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account); + callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account); return; case OpenPgpApi.RESULT_CODE_ERROR: logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); @@ -249,7 +248,7 @@ public class PgpEngine { callback.success(signatureBuilder.toString()); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status); + callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status); return; case OpenPgpApi.RESULT_CODE_ERROR: OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); @@ -276,7 +275,7 @@ public class PgpEngine { callback.success(contact); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact); + callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact); return; case OpenPgpApi.RESULT_CODE_ERROR: logError(contact.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 82592d7b5..91a08a0cd 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -64,8 +64,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable protected final JSONObject keys; private final Roster roster = new Roster(this); private final Collection blocklist = new CopyOnWriteArraySet<>(); - public List pendingConferenceJoins = new CopyOnWriteArrayList<>(); - public List pendingConferenceLeaves = new CopyOnWriteArrayList<>(); + public final Set pendingConferenceJoins = new HashSet<>(); + public final Set pendingConferenceLeaves = new HashSet<>(); + public final Set inProgressConferenceJoins = new HashSet<>(); + public final Set inProgressConferencePings = new HashSet<>(); protected Jid jid; protected String password; protected int options = 0; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 8b359713a..c9ec5637a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -54,6 +54,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; + public static final String ATTRIBUTE_PUSH_NODE = "push_node"; public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history"; static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message"; diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 8f96a21ab..62777f194 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -17,6 +17,7 @@ import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.utils.JidHelper; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Field; @@ -113,6 +114,10 @@ public class MucOptions { return MessageArchiveService.Version.has(getFeatures()); } + public boolean push() { + return getFeatures().contains(Namespace.PUSH); + } + public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) { this.serviceDiscoveryResult = serviceDiscoveryResult; String name; diff --git a/src/main/java/eu/siacs/conversations/entities/RawBlockable.java b/src/main/java/eu/siacs/conversations/entities/RawBlockable.java new file mode 100644 index 000000000..884b998d5 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/RawBlockable.java @@ -0,0 +1,87 @@ +package eu.siacs.conversations.entities; + +import android.content.Context; +import android.text.TextUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import eu.siacs.conversations.utils.UIHelper; +import rocks.xmpp.addr.Jid; + +public class RawBlockable implements ListItem, Blockable { + + private final Account account; + private final Jid jid; + + public RawBlockable(Account account, Jid jid) { + this.account = account; + this.jid = jid; + } + + @Override + public boolean isBlocked() { + return true; + } + + @Override + public boolean isDomainBlocked() { + throw new AssertionError("not implemented"); + } + + @Override + public Jid getBlockedJid() { + return this.jid; + } + + @Override + public String getDisplayName() { + if (jid.isFullJid()) { + return jid.getResource(); + } else { + return jid.toEscapedString(); + } + } + + @Override + public Jid getJid() { + return this.jid; + } + + @Override + public List getTags(Context context) { + return Collections.emptyList(); + } + + @Override + public boolean match(Context context, String needle) { + if (TextUtils.isEmpty(needle)) { + return true; + } + needle = needle.toLowerCase(Locale.US).trim(); + String[] parts = needle.split("\\s+"); + for (String part : parts) { + if (!jid.toEscapedString().contains(part)) { + return false; + } + } + return true; + } + + @Override + public Account getAccount() { + return account; + } + + @Override + public int getAvatarBackgroundColor() { + return UIHelper.getColorForName(jid.toEscapedString()); + } + + @Override + public int compareTo(ListItem o) { + return this.getDisplayName().compareToIgnoreCase( + o.getDisplayName()); + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 69af74ac2..d428e6743 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -305,7 +305,7 @@ public class IqGenerator extends AbstractGenerator { 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.asBareJid().toString()); + final Element item = block.addChild("item").setAttribute("jid", jid.toEscapedString()); if (reportSpam) { item.addChild("report", "urn:xmpp:reporting:0").addChild("spam"); } @@ -316,7 +316,7 @@ public class IqGenerator extends AbstractGenerator { 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.asBareJid().toString()); + block.addChild("item").setAttribute("jid", jid.toEscapedString()); return iq; } @@ -423,29 +423,60 @@ public class IqGenerator extends AbstractGenerator { } public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + return pushTokenToAppServer(appServer, token, deviceId, null); + } + + public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); packet.setTo(appServer); - Element command = packet.addChild("command", "http://jabber.org/protocol/commands"); + final Element command = packet.addChild("command", Namespace.COMMANDS); command.setAttribute("node", "register-push-fcm"); command.setAttribute("action", "execute"); - Data data = new Data(); + 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 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 IqPacket enablePush(Jid jid, String node, String secret) { + public IqPacket enablePush(final Jid jid, final String node, final String secret) { IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - Element enable = packet.addChild("enable", "urn:xmpp:push:0"); + Element enable = packet.addChild("enable", Namespace.PUSH); enable.setAttribute("jid", jid.toString()); enable.setAttribute("node", node); - Data data = new Data(); - data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS); - data.put("secret", secret); - data.submit(); - enable.addChild(data); + 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 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; } diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 3b4cfba90..3042e510f 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -26,6 +26,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Element; @@ -37,360 +38,383 @@ import rocks.xmpp.addr.Jid; public class IqParser extends AbstractParser implements OnIqPacketReceived { - public IqParser(final XmppConnectionService service) { - super(service); - } + public IqParser(final XmppConnectionService service) { + super(service); + } - private void rosterItems(final Account account, final Element query) { - final String version = query.getAttribute("ver"); - if (version != null) { - account.getRoster().setVersion(version); - } - for (final Element item : query.getChildren()) { - if (item.getName().equals("item")) { - final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); - if (jid == null) { - continue; - } - final String name = item.getAttribute("name"); - final String subscription = item.getAttribute("subscription"); - final Contact contact = account.getRoster().getContact(jid); - boolean bothPre = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM); - if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { - contact.setServerName(name); - contact.parseGroupsFromElement(item); - } - if ("remove".equals(subscription)) { - contact.resetOption(Contact.Options.IN_ROSTER); - contact.resetOption(Contact.Options.DIRTY_DELETE); - contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - } else { - contact.setOption(Contact.Options.IN_ROSTER); - contact.resetOption(Contact.Options.DIRTY_PUSH); - contact.parseSubscriptionFromElement(item); - } - boolean both = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM); - if ((both != bothPre) && both) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": gained mutual presence subscription with "+contact.getJid()); - AxolotlService axolotlService = account.getAxolotlService(); - if (axolotlService != null) { - axolotlService.clearErrorsInFetchStatusMap(contact.getJid()); - } - } - mXmppConnectionService.getAvatarService().clear(contact); - } - } - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); - mXmppConnectionService.getShortcutService().refresh(); - mXmppConnectionService.syncRoster(account); - } + private void rosterItems(final Account account, final Element query) { + final String version = query.getAttribute("ver"); + if (version != null) { + account.getRoster().setVersion(version); + } + for (final Element item : query.getChildren()) { + if (item.getName().equals("item")) { + final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); + if (jid == null) { + continue; + } + final String name = item.getAttribute("name"); + final String subscription = item.getAttribute("subscription"); + final Contact contact = account.getRoster().getContact(jid); + boolean bothPre = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM); + if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { + contact.setServerName(name); + contact.parseGroupsFromElement(item); + } + if ("remove".equals(subscription)) { + contact.resetOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else { + contact.setOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.parseSubscriptionFromElement(item); + } + boolean both = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM); + if ((both != bothPre) && both) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": gained mutual presence subscription with " + contact.getJid()); + AxolotlService axolotlService = account.getAxolotlService(); + if (axolotlService != null) { + axolotlService.clearErrorsInFetchStatusMap(contact.getJid()); + } + } + mXmppConnectionService.getAvatarService().clear(contact); + } + } + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateRosterUi(); + mXmppConnectionService.getShortcutService().refresh(); + mXmppConnectionService.syncRoster(account); + } - public String avatarData(final IqPacket packet) { - final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); - if (pubsub == null) { - return null; - } - final Element items = pubsub.findChild("items"); - if (items == null) { - return null; - } - return super.avatarData(items); - } + public String avatarData(final IqPacket packet) { + final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); + if (pubsub == null) { + return null; + } + final Element items = pubsub.findChild("items"); + if (items == null) { + return null; + } + return super.avatarData(items); + } - public Element getItem(final IqPacket packet) { - final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); - if (pubsub == null) { - return null; - } - final Element items = pubsub.findChild("items"); - if (items == null) { - return null; - } - return items.findChild("item"); - } + public Element getItem(final IqPacket packet) { + final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); + if (pubsub == null) { + return null; + } + final Element items = pubsub.findChild("items"); + if (items == null) { + return null; + } + return items.findChild("item"); + } - @NonNull - public Set deviceIds(final Element item) { - Set deviceIds = new HashSet<>(); - if (item != null) { - final Element list = item.findChild("list"); - if (list != null) { - for (Element device : list.getChildren()) { - if (!device.getName().equals("device")) { - continue; - } - try { - Integer id = Integer.valueOf(device.getAttribute("id")); - deviceIds.add(id); - } catch (NumberFormatException e) { - Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered invalid node in PEP ("+e.getMessage()+"):" + device.toString()+ ", skipping..."); - continue; - } - } - } - } - return deviceIds; - } + @NonNull + public Set deviceIds(final Element item) { + Set deviceIds = new HashSet<>(); + if (item != null) { + final Element list = item.findChild("list"); + if (list != null) { + for (Element device : list.getChildren()) { + if (!device.getName().equals("device")) { + continue; + } + try { + Integer id = Integer.valueOf(device.getAttribute("id")); + deviceIds.add(id); + } catch (NumberFormatException e) { + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Encountered invalid node in PEP (" + e.getMessage() + "):" + device.toString() + ", skipping..."); + continue; + } + } + } + } + return deviceIds; + } - public Integer signedPreKeyId(final Element bundle) { - final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); - if(signedPreKeyPublic == null) { - return null; - } - try { - return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId")); - } catch (NumberFormatException e) { - return null; - } - } + public Integer signedPreKeyId(final Element bundle) { + final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); + if (signedPreKeyPublic == null) { + return null; + } + try { + return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId")); + } catch (NumberFormatException e) { + return null; + } + } - public ECPublicKey signedPreKeyPublic(final Element bundle) { - ECPublicKey publicKey = null; - final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); - if(signedPreKeyPublic == null) { - return null; - } - try { - publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0); - } catch (Throwable e) { - Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage()); - } - return publicKey; - } + public ECPublicKey signedPreKeyPublic(final Element bundle) { + ECPublicKey publicKey = null; + final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); + if (signedPreKeyPublic == null) { + return null; + } + try { + publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(), Base64.DEFAULT), 0); + } catch (Throwable e) { + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Invalid signedPreKeyPublic in PEP: " + e.getMessage()); + } + return publicKey; + } - public byte[] signedPreKeySignature(final Element bundle) { - final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature"); - if(signedPreKeySignature == null) { - return null; - } - try { - return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT); - } catch (Throwable e) { - Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : Invalid base64 in signedPreKeySignature"); - return null; - } - } + public byte[] signedPreKeySignature(final Element bundle) { + final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature"); + if (signedPreKeySignature == null) { + return null; + } + try { + return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT); + } catch (Throwable e) { + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : Invalid base64 in signedPreKeySignature"); + return null; + } + } - public IdentityKey identityKey(final Element bundle) { - IdentityKey identityKey = null; - final Element identityKeyElement = bundle.findChild("identityKey"); - if(identityKeyElement == null) { - return null; - } - try { - identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); - } catch (Throwable e) { - Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage()); - } - return identityKey; - } + public IdentityKey identityKey(final Element bundle) { + IdentityKey identityKey = null; + final Element identityKeyElement = bundle.findChild("identityKey"); + if (identityKeyElement == null) { + return null; + } + try { + identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); + } catch (Throwable e) { + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Invalid identityKey in PEP: " + e.getMessage()); + } + return identityKey; + } - public Map preKeyPublics(final IqPacket packet) { - Map preKeyRecords = new HashMap<>(); - Element item = getItem(packet); - if (item == null) { - Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find in bundle IQ packet: " + packet); - return null; - } - final Element bundleElement = item.findChild("bundle"); - if(bundleElement == null) { - return null; - } - final Element prekeysElement = bundleElement.findChild("prekeys"); - if(prekeysElement == null) { - Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find in bundle IQ packet: " + packet); - return null; - } - for(Element preKeyPublicElement : prekeysElement.getChildren()) { - if(!preKeyPublicElement.getName().equals("preKeyPublic")){ - Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement); - continue; - } - Integer preKeyId = null; - try { - preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); - final ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0); - preKeyRecords.put(preKeyId, preKeyPublic); - } catch (NumberFormatException e) { - Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"could not parse preKeyId from preKey "+preKeyPublicElement.toString()); - } catch (Throwable e) { - Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); - } - } - return preKeyRecords; - } + public Map preKeyPublics(final IqPacket packet) { + Map preKeyRecords = new HashMap<>(); + Element item = getItem(packet); + if (item == null) { + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Couldn't find in bundle IQ packet: " + packet); + return null; + } + final Element bundleElement = item.findChild("bundle"); + if (bundleElement == null) { + return null; + } + final Element prekeysElement = bundleElement.findChild("prekeys"); + if (prekeysElement == null) { + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Couldn't find in bundle IQ packet: " + packet); + return null; + } + for (Element preKeyPublicElement : prekeysElement.getChildren()) { + if (!preKeyPublicElement.getName().equals("preKeyPublic")) { + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Encountered unexpected tag in prekeys list: " + preKeyPublicElement); + continue; + } + Integer preKeyId = null; + try { + preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); + final ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0); + preKeyRecords.put(preKeyId, preKeyPublic); + } catch (NumberFormatException e) { + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "could not parse preKeyId from preKey " + preKeyPublicElement.toString()); + } catch (Throwable e) { + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Invalid preKeyPublic (ID=" + preKeyId + ") in PEP: " + e.getMessage() + ", skipping..."); + } + } + return preKeyRecords; + } - public Pair verification(final IqPacket packet) { - Element item = getItem(packet); - Element verification = item != null ? item.findChild("verification",AxolotlService.PEP_PREFIX) : null; - Element chain = verification != null ? verification.findChild("chain") : null; - Element signature = verification != null ? verification.findChild("signature") : null; - if (chain != null && signature != null) { - List certElements = chain.getChildren(); - X509Certificate[] certificates = new X509Certificate[certElements.size()]; - try { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - int i = 0; - for(Element cert : certElements) { - certificates[i] = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.getContent(),Base64.DEFAULT))); - ++i; - } - return new Pair<>(certificates,Base64.decode(signature.getContent(),Base64.DEFAULT)); - } catch (CertificateException e) { - return null; - } - } else { - return null; - } - } + public Pair verification(final IqPacket packet) { + Element item = getItem(packet); + Element verification = item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null; + Element chain = verification != null ? verification.findChild("chain") : null; + Element signature = verification != null ? verification.findChild("signature") : null; + if (chain != null && signature != null) { + List certElements = chain.getChildren(); + X509Certificate[] certificates = new X509Certificate[certElements.size()]; + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + int i = 0; + for (Element cert : certElements) { + certificates[i] = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.getContent(), Base64.DEFAULT))); + ++i; + } + return new Pair<>(certificates, Base64.decode(signature.getContent(), Base64.DEFAULT)); + } catch (CertificateException e) { + return null; + } + } else { + return null; + } + } - public PreKeyBundle bundle(final IqPacket bundle) { - Element bundleItem = getItem(bundle); - if(bundleItem == null) { - return null; - } - final Element bundleElement = bundleItem.findChild("bundle"); - if(bundleElement == null) { - return null; - } - ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); - Integer signedPreKeyId = signedPreKeyId(bundleElement); - byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); - IdentityKey identityKey = identityKey(bundleElement); - if(signedPreKeyId == null || signedPreKeyPublic == null || identityKey == null) { - return null; - } + public PreKeyBundle bundle(final IqPacket bundle) { + Element bundleItem = getItem(bundle); + if (bundleItem == null) { + return null; + } + final Element bundleElement = bundleItem.findChild("bundle"); + if (bundleElement == null) { + return null; + } + ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); + Integer signedPreKeyId = signedPreKeyId(bundleElement); + byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); + IdentityKey identityKey = identityKey(bundleElement); + if (signedPreKeyId == null || signedPreKeyPublic == null || identityKey == null) { + return null; + } - return new PreKeyBundle(0, 0, 0, null, - signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); - } + return new PreKeyBundle(0, 0, 0, null, + signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); + } - public List preKeys(final IqPacket preKeys) { - List bundles = new ArrayList<>(); - Map preKeyPublics = preKeyPublics(preKeys); - if ( preKeyPublics != null) { - for (Integer preKeyId : preKeyPublics.keySet()) { - ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); - bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, - 0, null, null, null)); - } - } + public List preKeys(final IqPacket preKeys) { + List bundles = new ArrayList<>(); + Map preKeyPublics = preKeyPublics(preKeys); + if (preKeyPublics != null) { + for (Integer preKeyId : preKeyPublics.keySet()) { + ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); + bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, + 0, null, null, null)); + } + } - return bundles; - } + return bundles; + } - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - final boolean isGet = packet.getType() == IqPacket.TYPE.GET; - if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; - } - if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) { - final Element query = packet.findChild("query"); - // If this is in response to a query for the whole roster: - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getRoster().markAllAsNotInRoster(); - } - this.rosterItems(account, query); - } else if ((packet.hasChild("block", Namespace.BLOCKING) || packet.hasChild("blocklist", Namespace.BLOCKING)) && - packet.fromServer(account)) { - // Block list or block push. - Log.d(Config.LOGTAG, "Received blocklist update from server"); - final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING); - final Element block = packet.findChild("block", Namespace.BLOCKING); - final Collection items = blocklist != null ? blocklist.getChildren() : - (block != null ? block.getChildren() : null); - // If this is a response to a blocklist query, clear the block list and replace with the new one. - // Otherwise, just update the existing blocklist. - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.clearBlocklist(); - account.getXmppConnection().getFeatures().setBlockListRequested(true); - } - if (items != null) { - final Collection jids = new ArrayList<>(items.size()); - // Create a collection of Jids from the packet - for (final Element item : items) { - if (item.getName().equals("item")) { - final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); - if (jid != null) { - jids.add(jid); - } - } - } - account.getBlocklist().addAll(jids); - if (packet.getType() == IqPacket.TYPE.SET) { - boolean removed = false; - for(Jid jid : jids) { - removed |= mXmppConnectionService.removeBlockedConversations(account,jid); - } - if (removed) { - mXmppConnectionService.updateConversationUi(); - } - } - } - // Update the UI - mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - if (packet.getType() == IqPacket.TYPE.SET) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } - } else if (packet.hasChild("unblock", Namespace.BLOCKING) && - packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) { - Log.d(Config.LOGTAG, "Received unblock update from server"); - final Collection items = packet.findChild("unblock", Namespace.BLOCKING).getChildren(); - if (items.size() == 0) { - // No children to unblock == unblock all - account.getBlocklist().clear(); - } else { - final Collection jids = new ArrayList<>(items.size()); - for (final Element item : items) { - if (item.getName().equals("item")) { - final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); - if (jid != null) { - jids.add(jid); - } - } - } - account.getBlocklist().removeAll(jids); - } - mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") - || packet.hasChild("data", "http://jabber.org/protocol/ibb") - || packet.hasChild("close","http://jabber.org/protocol/ibb")) { - mXmppConnectionService.getJingleConnectionManager() - .deliverIbbPacket(account, packet); - } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { - final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("query","jabber:iq:version") && isGet) { - final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet); - mXmppConnectionService.sendIqPacket(account,response,null); - } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("time","urn:xmpp:time") && isGet) { - final IqPacket response; - if (mXmppConnectionService.useTorToConnect() || account.isOnion()) { - response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type","cancel"); - error.addChild("not-allowed","urn:ietf:params:xml:ns:xmpp-stanzas"); - } else { - response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); - } - mXmppConnectionService.sendIqPacket(account,response, null); - } else { - if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("feature-not-implemented","urn:ietf:params:xml:ns:xmpp-stanzas"); - account.getXmppConnection().sendIqPacket(response, null); - } - } - } + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final boolean isGet = packet.getType() == IqPacket.TYPE.GET; + if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) { + return; + } + if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) { + final Element query = packet.findChild("query"); + // If this is in response to a query for the whole roster: + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.getRoster().markAllAsNotInRoster(); + } + this.rosterItems(account, query); + } else if ((packet.hasChild("block", Namespace.BLOCKING) || packet.hasChild("blocklist", Namespace.BLOCKING)) && + packet.fromServer(account)) { + // Block list or block push. + Log.d(Config.LOGTAG, "Received blocklist update from server"); + final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING); + final Element block = packet.findChild("block", Namespace.BLOCKING); + final Collection items = blocklist != null ? blocklist.getChildren() : + (block != null ? block.getChildren() : null); + // If this is a response to a blocklist query, clear the block list and replace with the new one. + // Otherwise, just update the existing blocklist. + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.clearBlocklist(); + account.getXmppConnection().getFeatures().setBlockListRequested(true); + } + if (items != null) { + final Collection jids = new ArrayList<>(items.size()); + // Create a collection of Jids from the packet + for (final Element item : items) { + if (item.getName().equals("item")) { + final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); + if (jid != null) { + jids.add(jid); + } + } + } + account.getBlocklist().addAll(jids); + if (packet.getType() == IqPacket.TYPE.SET) { + boolean removed = false; + for (Jid jid : jids) { + removed |= mXmppConnectionService.removeBlockedConversations(account, jid); + } + if (removed) { + mXmppConnectionService.updateConversationUi(); + } + } + } + // Update the UI + mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); + if (packet.getType() == IqPacket.TYPE.SET) { + final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + mXmppConnectionService.sendIqPacket(account, response, null); + } + } else if (packet.hasChild("unblock", Namespace.BLOCKING) && + packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) { + Log.d(Config.LOGTAG, "Received unblock update from server"); + final Collection items = packet.findChild("unblock", Namespace.BLOCKING).getChildren(); + if (items.size() == 0) { + // No children to unblock == unblock all + account.getBlocklist().clear(); + } else { + final Collection jids = new ArrayList<>(items.size()); + for (final Element item : items) { + if (item.getName().equals("item")) { + final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); + if (jid != null) { + jids.add(jid); + } + } + } + account.getBlocklist().removeAll(jids); + } + mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); + final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") + || packet.hasChild("data", "http://jabber.org/protocol/ibb") + || packet.hasChild("close", "http://jabber.org/protocol/ibb")) { + mXmppConnectionService.getJingleConnectionManager() + .deliverIbbPacket(account, packet); + } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { + final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet); + mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasChild("query", "jabber:iq:version") && isGet) { + final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet); + mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) { + final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) { + final IqPacket response; + if (mXmppConnectionService.useTorToConnect() || account.isOnion()) { + response = packet.generateResponse(IqPacket.TYPE.ERROR); + final Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas"); + } else { + response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); + } + mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasChild("pubsub", Namespace.PUBSUB) && packet.getType() == IqPacket.TYPE.SET) { + final Jid server = packet.getFrom(); + final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); + final Element publish = pubsub == null ? null : pubsub.findChild("publish"); + final String node = publish == null ? null : publish.getAttribute("node"); + final Element item = publish == null ? null : publish.findChild("item"); + final Element notification = item == null ? null : item.findChild("notification", Namespace.PUSH); + if (notification != null && node != null && server != null) { + final Conversation conversation = mXmppConnectionService.findConversationByUuid(node); + if (conversation != null && conversation.getAccount() == account && conversation.getJid().getDomain().equals(server.getDomain())) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received muc push event for "+conversation.getJid().asBareJid()); + mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); + mXmppConnectionService.mucSelfPingAndRejoin(conversation); + } else { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received push event for unknown conference from "+server); + final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); + final Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); + mXmppConnectionService.sendIqPacket(account, response, null); + } + } + + } else { + if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { + final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); + final Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas"); + account.getXmppConnection().sendIqPacket(response, null); + } + } + } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 72aaf1fa0..5adaffe34 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -22,6 +22,7 @@ import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.ReadByMarker; @@ -126,7 +127,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece service.reportBrokenSessionException(e, postpone); return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); } else { - Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicase failed"); + Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicates failed"); + //TODO should be still emit a failed message? return null; } } catch (NotEncryptedForThisDeviceException e) { @@ -264,6 +266,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece packet.getId(), Message.STATUS_SEND_FAILED, extractErrorMessage(packet)); + final Element error = packet.findChild("error"); + final boolean pingWorthyError = error != null && (error.hasChild("not-acceptable") || error.hasChild("remote-server-timeout") || error.hasChild("remote-server-not-found")); + if (pingWorthyError) { + Conversation conversation = mXmppConnectionService.find(account,from); + if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) { + if (conversation.getMucOptions().online()) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received ping worthy error for seemingly online muc at "+from); + mXmppConnectionService.mucSelfPingAndRejoin(conversation); + } + } + } } return true; } @@ -437,6 +450,7 @@ 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); if (origin != null) { @@ -598,7 +612,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } else { serverMsgIdUpdated = false; } - Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + Boolean.toString(serverMsgIdUpdated)); + Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + serverMsgIdUpdated); return; } } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index b6f5dc518..8c77bdad4 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -341,7 +341,7 @@ public class FileBackend { } } - public static void close(Closeable stream) { + public static void close(final Closeable stream) { if (stream != null) { try { stream.close(); @@ -350,7 +350,7 @@ public class FileBackend { } } - public static void close(Socket socket) { + public static void close(final Socket socket) { if (socket != null) { try { socket.close(); diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index 17e05929c..0e349e508 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -37,6 +37,7 @@ import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.RawBlockable; import eu.siacs.conversations.http.services.MuclumbusService; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; @@ -272,7 +273,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded { } public Bitmap get(ListItem item, int size, boolean cachedOnly) { - if (item instanceof Contact) { + if (item instanceof RawBlockable) { + return get(item.getDisplayName(), item.getJid().toEscapedString(), size, cachedOnly); + } else if (item instanceof Contact) { return get((Contact) item, size, cachedOnly); } else if (item instanceof Bookmark) { Bookmark bookmark = (Bookmark) item; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index a596f3b87..0c6df6d74 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -314,6 +314,12 @@ public class XmppConnectionService extends Service { } account.getRoster().clearPresences(); + synchronized (account.inProgressConferenceJoins) { + account.inProgressConferenceJoins.clear(); + } + synchronized (account.inProgressConferencePings) { + account.inProgressConferencePings.clear(); + } mJingleConnectionManager.cancelInTransmission(); mQuickConversationsService.considerSyncBackground(false); fetchRosterFromServer(account); @@ -372,18 +378,37 @@ public class XmppConnectionService extends Service { } List conversations = getConversations(); for (Conversation conversation : conversations) { - if (conversation.getAccount() == account && !account.pendingConferenceJoins.contains(conversation)) { + final boolean inProgressJoin; + synchronized (account.inProgressConferenceJoins) { + inProgressJoin = account.inProgressConferenceJoins.contains(conversation); + } + final boolean pendingJoin; + synchronized (account.pendingConferenceJoins) { + pendingJoin = account.pendingConferenceJoins.contains(conversation); + } + if (conversation.getAccount() == account + && !pendingJoin + && !inProgressJoin) { sendUnsentMessages(conversation); } } - for (Conversation conversation : account.pendingConferenceLeaves) { + final List pendingLeaves; + synchronized (account.pendingConferenceLeaves) { + pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves); + account.pendingConferenceLeaves.clear(); + + } + for (Conversation conversation : pendingLeaves) { leaveMuc(conversation); } - account.pendingConferenceLeaves.clear(); - for (Conversation conversation : account.pendingConferenceJoins) { + final List pendingJoins; + synchronized (account.pendingConferenceJoins) { + pendingJoins = new ArrayList<>(account.pendingConferenceJoins); + account.pendingConferenceJoins.clear(); + } + for (Conversation conversation : pendingJoins) { joinMuc(conversation); } - account.pendingConferenceJoins.clear(); scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) { resetSendingToWaiting(account); @@ -586,6 +611,7 @@ public class XmppConnectionService extends Service { toggleForegroundService(true); } String pushedAccountHash = null; + String pushedChannelHash = null; boolean interactive = false; if (action != null) { final String uuid = intent.getStringExtra("uuid"); @@ -698,6 +724,7 @@ public class XmppConnectionService extends Service { break; case ACTION_FCM_MESSAGE_RECEIVED: pushedAccountHash = intent.getStringExtra("account"); + pushedChannelHash = intent.getStringExtra("channel"); Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash); break; case Intent.ACTION_SEND: @@ -711,13 +738,18 @@ public class XmppConnectionService extends Service { synchronized (this) { WakeLockHelper.acquire(wakeLock); boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); - HashSet pingCandidates = new HashSet<>(); + final HashSet pingCandidates = new HashSet<>(); + final String androidId = PhoneHelper.getAndroidId(this); for (Account account : accounts) { + final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash); pingNow |= processAccountState(account, interactive, "ui".equals(action), - CryptoHelper.getAccountFingerprint(account, PhoneHelper.getAndroidId(this)).equals(pushedAccountHash), + pushWasMeantForThisAccount, pingCandidates); + if (pushWasMeantForThisAccount && pushedChannelHash != null) { + checkMucStillJoined(account, pushedAccountHash, androidId); + } } if (pingNow) { for (Account account : pingCandidates) { @@ -810,6 +842,20 @@ public class XmppConnectionService extends Service { return pingNow; } + private void checkMucStillJoined(final Account account, final String hash, final String androidId) { + 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); + return; + } + } + } + mPushManagementService.unregisterChannel(account, hash); + } + public void reinitializeMuclumbusService() { mChannelDiscoveryService.initializeMuclumbusService(); } @@ -848,7 +894,7 @@ public class XmppConnectionService extends Service { } @Override - public void userInputRequried(PendingIntent pi, Message object) { + public void userInputRequired(PendingIntent pi, Message object) { } }); @@ -1347,7 +1393,12 @@ public class XmppConnectionService extends Service { } } - if (account.isOnlineAndConnected()) { + final boolean inProgressJoin; + synchronized (account.inProgressConferenceJoins) { + inProgressJoin = conversation.getMode() == Conversational.MODE_MULTI && account.inProgressConferenceJoins.contains(conversation); + } + + if (account.isOnlineAndConnected() && !inProgressJoin) { switch (message.getEncryption()) { case Message.ENCRYPTION_NONE: if (message.needsUploading()) { @@ -1995,6 +2046,10 @@ public class XmppConnectionService extends Service { } } } + if (conversation.getMucOptions().push()) { + disableDirectMucPush(conversation); + mPushManagementService.disablePushOnServer(conversation); + } leaveMuc(conversation); } else { if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { @@ -2432,21 +2487,37 @@ public class XmppConnectionService extends Service { } 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; + } + } 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, (account, response) -> { + 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,account.getJid().asBareJid()+": ping to "+self+" came back as ignorable error"); + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": ping to "+self+" came back as ignorable error"); } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ping to "+self+" failed. attempting rejoin"); + 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,account.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); } }); } @@ -2464,10 +2535,17 @@ public class XmppConnectionService extends Service { } private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) { - Account account = conversation.getAccount(); - account.pendingConferenceJoins.remove(conversation); - account.pendingConferenceLeaves.remove(conversation); + 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) { + account.inProgressConferenceJoins.add(conversation); + } sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions())); conversation.resetMucOptions(); if (onConferenceJoined != null) { @@ -2523,7 +2601,13 @@ public class XmppConnectionService extends Service { saveConversationAsBookmark(conversation, null); } } - sendUnsentMessages(conversation); + if (mucOptions.push()) { + enableMucPush(conversation); + } + synchronized (account.inProgressConferenceJoins) { + account.inProgressConferenceJoins.remove(conversation); + sendUnsentMessages(conversation); + } } @Override @@ -2539,9 +2623,13 @@ public class XmppConnectionService extends Service { 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) { + account.inProgressConferenceJoins.remove(conversation); + } conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND); updateConversationUi(); } else { @@ -2552,13 +2640,48 @@ public class XmppConnectionService extends Service { }); updateConversationUi(); } else { - account.pendingConferenceJoins.add(conversation); + synchronized (account.pendingConferenceJoins) { + account.pendingConferenceJoins.add(conversation); + } conversation.resetMucOptions(); conversation.setHasMessagesLeftOnServer(false); updateConversationUi(); } } + 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); + } else if (response.getType() == IqPacket.TYPE.ERROR) { + 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); + mPushManagementService.registerPushTokenOnServer(conversation); + } + + private void disableDirectMucPush(final Conversation conversation) { + final Account account = conversation.getAccount(); + final Jid room = conversation.getJid().asBareJid(); + final IqPacket disable = mIqGenerator.disablePush(conversation.getAccount().getJid(), conversation.getUuid()); + 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); + } else if (response.getType() == IqPacket.TYPE.ERROR) { + 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(); @@ -2734,9 +2857,13 @@ public class XmppConnectionService extends Service { } private void leaveMuc(Conversation conversation, boolean now) { - Account account = conversation.getAccount(); - account.pendingConferenceJoins.remove(conversation); - account.pendingConferenceLeaves.remove(conversation); + 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(); @@ -2746,7 +2873,9 @@ public class XmppConnectionService extends Service { } Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid()); } else { - account.pendingConferenceLeaves.add(conversation); + synchronized (account.pendingConferenceLeaves) { + account.pendingConferenceLeaves.add(conversation); + } } } @@ -4007,6 +4136,7 @@ public class XmppConnectionService extends Service { for (Account account : getAccounts()) { if (account.isOnlineAndConnected() && mPushManagementService.available(account)) { mPushManagementService.registerPushTokenOnServer(account); + //TODO renew mucs } } } @@ -4121,17 +4251,15 @@ public class XmppConnectionService extends Service { 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), new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getBlocklist().add(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - } - } - }); - if (removeBlockedConversations(blockable.getAccount(), jid)) { + 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; + } else if (removeBlockedConversations(blockable.getAccount(), jid)) { updateConversationUi(); return true; } else { diff --git a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java index e951ff492..68d302b03 100644 --- a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java @@ -28,14 +28,18 @@ public final class BlockContactDialog { final String value; @StringRes int res; - if (blockable.getJid().getLocal() == null || blockable.getAccount().isBlocked(Jid.ofDomain(blockable.getJid().getDomain()))) { + if (blockable.getJid().isFullJid()) { + builder.setTitle(isBlocked ? R.string.action_unblock_participant : R.string.action_block_participant); + value = blockable.getJid().toEscapedString(); + res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text; + } else if (blockable.getJid().getLocal() == null || blockable.getAccount().isBlocked(Jid.ofDomain(blockable.getJid().getDomain()))) { builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain); value = Jid.ofDomain(blockable.getJid().getDomain()).toString(); res = isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text; } else { int resBlockAction = blockable instanceof Conversation && ((Conversation) blockable).isWithStranger() ? R.string.block_stranger : R.string.action_block_contact; builder.setTitle(isBlocked ? R.string.action_unblock_contact : resBlockAction); - value = blockable.getJid().asBareJid().toString(); + value = blockable.getJid().asBareJid().toEscapedString(); res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text; } binding.text.setText(JidDialog.style(xmppActivity, res, value)); diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java index 74d2d3392..df091e7df 100644 --- a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java @@ -10,7 +10,10 @@ import java.util.Collections; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.entities.RawBlockable; import eu.siacs.conversations.ui.interfaces.OnBackendConnected; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import rocks.xmpp.addr.Jid; @@ -23,7 +26,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); getListView().setOnItemLongClickListener((parent, view, position, id) -> { - BlockContactDialog.show(BlocklistActivity.this, (Contact) getListItems().get(position)); + BlockContactDialog.show(BlocklistActivity.this, (Blockable) getListItems().get(position)); return true; }); this.binding.fab.show(); @@ -50,9 +53,14 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem getListItems().clear(); if (account != null) { for (final Jid jid : account.getBlocklist()) { - final Contact contact = account.getRoster().getContact(jid); - if (contact.match(this, needle) && contact.isBlocked()) { - getListItems().add(contact); + ListItem item; + if (jid.isFullJid()) { + item = new RawBlockable(account, jid); + } else { + item = account.getRoster().getContact(jid); + } + if (item.match(this, needle)) { + getListItems().add(item); } } Collections.sort(getListItems()); @@ -78,8 +86,8 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem ); dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> { - Contact contact = account.getRoster().getContact(contactJid); - if (xmppConnectionService.sendBlockRequest(contact, false)) { + Blockable blockable = new RawBlockable(account, contactJid); + if (xmppConnectionService.sendBlockRequest(blockable, false)) { Toast.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show(); } return true; @@ -101,4 +109,5 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) { refreshUi(); } + } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 48abb03fe..ee2a15379 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -83,7 +83,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } @Override - public void userInputRequried(PendingIntent pi, Conversation object) { + public void userInputRequired(PendingIntent pi, Conversation object) { } }; diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 7a25a9d4d..3b4842437 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -632,7 +632,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } @Override - public void userInputRequried(PendingIntent pi, Message object) { + public void userInputRequired(PendingIntent pi, Message object) { } }); @@ -666,7 +666,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } @Override - public void userInputRequried(PendingIntent pi, Message message) { + public void userInputRequired(PendingIntent pi, Message message) { hidePrepareFileToast(prepareFileToast); } }); @@ -688,7 +688,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke new UiCallback() { @Override - public void userInputRequried(PendingIntent pi, Message object) { + public void userInputRequired(PendingIntent pi, Message object) { hidePrepareFileToast(prepareFileToast); } @@ -1326,7 +1326,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke new UiCallback() { @Override - public void userInputRequried(PendingIntent pi, Contact contact) { + public void userInputRequired(PendingIntent pi, Contact contact) { startPendingIntent(pi, attachmentChoice); } @@ -2284,7 +2284,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke status = Presence.Status.OFFLINE; } this.binding.textSendButton.setTag(action); - this.binding.textSendButton.setImageResource(SendButtonTool.getSendButtonImageResource(getActivity(), action, status)); + final Activity activity = getActivity(); + if (activity != null) { + this.binding.textSendButton.setImageResource(SendButtonTool.getSendButtonImageResource(activity, action, status)); + } } protected void updateStatusMessages() { @@ -2456,7 +2459,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke new UiCallback() { @Override - public void userInputRequried(PendingIntent pi, Contact contact) { + public void userInputRequired(PendingIntent pi, Contact contact) { startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE); } @@ -2512,7 +2515,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke new UiCallback() { @Override - public void userInputRequried(PendingIntent pi, Message message) { + public void userInputRequired(PendingIntent pi, Message message) { startPendingIntent(pi, REQUEST_SEND_MESSAGE); } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 71a4b9ae3..d8c208dd4 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -104,7 +104,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private final UiCallback mAvatarFetchCallback = new UiCallback() { @Override - public void userInputRequried(final PendingIntent pi, final Avatar avatar) { + public void userInputRequired(final PendingIntent pi, final Avatar avatar) { finishInitialSetup(avatar); } @@ -917,7 +917,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } @Override - public void userInputRequried(PendingIntent pi, String object) { + public void userInputRequired(PendingIntent pi, String object) { mPendingPresenceTemplate.push(template); try { startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0); diff --git a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java index 0367db8e1..2bddf5b2d 100644 --- a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java @@ -23,8 +23,10 @@ import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; import org.osmdroid.config.IConfigurationProvider; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.tileprovider.tilesource.XYTileSource; import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.CustomZoomButtonsController; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.Overlay; @@ -72,15 +74,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca protected void updateLocationMarkers() { clearMarkers(); } - - protected XYTileSource tileSource() { - return new XYTileSource("OpenStreetMap", - 0, 19, 256, ".png", new String[] { - "https://a.tile.openstreetmap.org/", - "https://b.tile.openstreetmap.org/", - "https://c.tile.openstreetmap.org/" },"© OpenStreetMap contributors"); - } - + @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -103,7 +97,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca final IConfigurationProvider config = Configuration.getInstance(); config.load(ctx, getPreferences()); - config.setUserAgentValue(BuildConfig.APPLICATION_ID + "_" + BuildConfig.VERSION_CODE); + config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE); if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) { try { config.setHttpProxy(HttpConnectionManager.getProxy()); @@ -111,17 +105,6 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca throw new RuntimeException("Unable to configure proxy"); } } - - final File f = new File(ctx.getCacheDir() + "/tiles"); - try { - //noinspection ResultOfMethodCallIgnored - f.mkdirs(); - } catch (final SecurityException ignored) { - } - if (f.exists() && f.isDirectory() && f.canRead() && f.canWrite()) { - Log.d(Config.LOGTAG, "Using tile cache at: " + f.getAbsolutePath()); - config.setOsmdroidTileCache(f.getAbsoluteFile()); - } } @Override @@ -150,8 +133,8 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca protected void setupMapView(MapView mapView, final GeoPoint pos) { map = mapView; - map.setTileSource(tileSource()); - map.setBuiltInZoomControls(false); + map.setTileSource(TileSourceFactory.MAPNIK); + map.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER); map.setMultiTouchControls(true); map.setTilesScaledToDpi(true); mapController = map.getController(); @@ -251,7 +234,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca requestLocationUpdates(); updateLocationMarkers(); updateUi(); - map.setTileSource(tileSource()); + map.setTileSource(TileSourceFactory.MAPNIK); map.setTilesScaledToDpi(true); if (mapAtInitialLoc()) { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index a05c707c9..a48dd230b 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -174,7 +174,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void userInputRequried(PendingIntent pi, Conversation object) { + public void userInputRequired(PendingIntent pi, Conversation object) { } }; @@ -1085,7 +1085,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void userInputRequried(PendingIntent pi, Conversation object) { + public void userInputRequired(PendingIntent pi, Conversation object) { } }); diff --git a/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/src/main/java/eu/siacs/conversations/ui/UiCallback.java index d056d6289..c690b4105 100644 --- a/src/main/java/eu/siacs/conversations/ui/UiCallback.java +++ b/src/main/java/eu/siacs/conversations/ui/UiCallback.java @@ -7,5 +7,5 @@ public interface UiCallback { void error(int errorCode, T object); - void userInputRequried(PendingIntent pi, T object); + void userInputRequired(PendingIntent pi, T object); } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 1bf41d4d2..f476736fa 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -137,7 +137,7 @@ public abstract class XmppActivity extends ActionBarActivity { } @Override - public void userInputRequried(PendingIntent pi, Conversation object) { + public void userInputRequired(PendingIntent pi, Conversation object) { } }; @@ -565,7 +565,7 @@ public abstract class XmppActivity extends ActionBarActivity { xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback() { @Override - public void userInputRequried(PendingIntent pi, String signature) { + public void userInputRequired(PendingIntent pi, String signature) { try { startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); } catch (final SendIntentException ignored) { @@ -625,7 +625,7 @@ public abstract class XmppActivity extends ActionBarActivity { } @Override - public void userInputRequried(PendingIntent pi, Account object) { + public void userInputRequired(PendingIntent pi, Account object) { try { startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0); diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 40b3cad65..2b15812f3 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -246,8 +246,12 @@ public final class CryptoHelper { return prettifyFingerprintCert(bytesToHex(fingerprint)); } + public static String getFingerprint(Jid jid, String androidId) { + return getFingerprint(jid.toEscapedString() + "\00" + androidId); + } + public static String getAccountFingerprint(Account account, String androidId) { - return getFingerprint(account.getJid().asBareJid().toEscapedString() + "\00" + androidId); + return getFingerprint(account.getJid().asBareJid(), androidId); } public static String getFingerprint(String value) { diff --git a/src/main/java/eu/siacs/conversations/utils/XEP0392Helper.java b/src/main/java/eu/siacs/conversations/utils/XEP0392Helper.java index a482c80ac..ee6a90e1e 100644 --- a/src/main/java/eu/siacs/conversations/utils/XEP0392Helper.java +++ b/src/main/java/eu/siacs/conversations/utils/XEP0392Helper.java @@ -7,7 +7,7 @@ import org.hsluv.HUSLColorConverter; import java.security.MessageDigest; -public class XEP0392Helper { +class XEP0392Helper { private static double angle(String nickname) { try { @@ -20,7 +20,7 @@ public class XEP0392Helper { } } - public static int rgbFromNick(String name) { + static int rgbFromNick(String name) { double[] hsluv = new double[3]; hsluv[0] = angle(name) * 360; hsluv[1] = 100; diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 73d3d452d..dbab039d7 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -28,4 +28,6 @@ public final class Namespace { public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1"; public static final String PING = "urn:xmpp:ping"; + public static final String PUSH = "urn:xmpp:push:0"; + public static final String COMMANDS = "http://jabber.org/protocol/commands"; } diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index ef7b73c77..1540e5f61 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -6,14 +6,15 @@ import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import eu.siacs.conversations.Config; -public class XmlReader { - private XmlPullParser parser; +public class XmlReader implements Closeable { + private final XmlPullParser parser; private InputStream is; public XmlReader() { @@ -48,6 +49,11 @@ public class XmlReader { } } + @Override + public void close() { + this.is = null; + } + public Tag readTag() throws IOException { try { while (this.is != null && parser.next() != XmlPullParser.END_DOCUMENT) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index a0fc5837d..68afc030f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1439,15 +1439,8 @@ public class XmppConnection implements Runnable { } private void forceCloseSocket() { - if (socket != null) { - try { - socket.close(); - } catch (IOException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception " + e.getMessage() + " during force close"); - } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": socket was null during force close"); - } + FileBackend.close(this.socket); + FileBackend.close(this.tagReader); } public void interrupt() { @@ -1458,7 +1451,7 @@ public class XmppConnection implements Runnable { public void disconnect(final boolean force) { interrupt(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + Boolean.toString(force)); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force); if (force) { forceCloseSocket(); } else { @@ -1798,8 +1791,8 @@ public class XmppConnection implements Runnable { } public boolean push() { - return hasDiscoFeature(account.getJid().asBareJid(), "urn:xmpp:push:0") - || hasDiscoFeature(Jid.of(account.getServer()), "urn:xmpp:push:0"); + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH) + || hasDiscoFeature(Jid.of(account.getServer()), Namespace.PUSH); } public boolean rosterVersioning() { diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index e0d90d845..1d51c5d48 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -776,4 +776,10 @@ Отваряне с… Профилна снимка за Conversations Изберете профил + Възстановяване от резервно копие + Възстановяване + Въведете паролата си за профила %s, за да направите възстановяване от резервно копие. + Не използвайте възможността за възстановяване от резервно копие, за да клонирате (да изпълнявате едновременно) инсталацията. Възстановяването от резервно копие е предназначено за мигриране или в случай, че сте загубили устройството си. + Не може да се направи възстановяване от резервно копие. + Резервното копие не може да бъде дешифрирано. Правилна ли е паролата? diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 374e355d8..1304adf48 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -17,6 +17,8 @@ Kontakt entsperren Domain sperren Domain entsperren + Teilnehmer sperren + Teilnehmer entsperren Konten verwalten Einstellungen Mit Unterhaltung teilen diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 34473b9cb..7801384e1 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -17,6 +17,8 @@ Desbloquear contacto Bloquear dominio Desbloquear dominio + Bloquear persoa + Desbloquear persoa Xestionar contas Axustes Compartir na conversa diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 239aafce8..bbd064227 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -17,6 +17,8 @@ Deblochează contact Blochează domeniu Deblochează domeniu + Blochează participant + Deblochează participant Configurează conturile Setări Partajează într-o conversație diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 9e6f7152c..28a174106 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -3,8 +3,10 @@ Настройки Новая беседа Управление аккаунтами + Закрыть текущую беседу Сведения о контакте Подробности конференции + Сведения о канале Защищённая беседа Добавить аккаунт Редактировать контакт @@ -48,6 +50,7 @@ Поделиться с Начать беседу Пригласить собеседника + Пригласить Контакты Контакт Отмена @@ -78,6 +81,7 @@ Вы хотите удалить все сообщения в этой беседе?\n\nПредупреждение: Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах. Удалить файл Вы уверены, что хотите удалить этот файл?\n\nПредупреждение: Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах. + Закрыть эту беседу Выберите устройство Нешифрованное сообщение Сообщение @@ -158,15 +162,18 @@ Временно отключить Разместить аватар Анонсировать OpenPGP ключ - Удалить открытый OpenPGP ключ + Удалить открытый ключ OpenPGP Вы действительно хотите удалить ваш OpenPGP публичный ключ из опубликованных?\nВаши собеседники не смогут больше отправлять вам зашифрованные OpenPGP сообщения. Открытый ключ OpenPGP был опубликован. Включить аккаунт Вы уверены? Если вы удалите аккаунт, будет потеряна вся история переписки. Запись голоса + XMPP-адрес + Заблокировать XMPP-адрес username@example.com Пароль + Недопустимый XMPP-адрес Недостаточно памяти. Изображение слишком большое Вы хотите добавить %s в вашу адресную книгу? Информация о сервере @@ -201,6 +208,7 @@ Получение ключей… Готово Расшифровать + Закладки Поиск Добавить контакт Удалить контакт @@ -213,6 +221,8 @@ Присоединиться Сохранить закладку Удалить закладку + Уничтожить конференцию + Уничтожить канал Такая закладка уже существует Редактировать тему конференции Тема @@ -253,6 +263,7 @@ Позволить контактам редактировать сообщения Расширенные настройки Пожалуйста, будьте осторожны с данными настройками + О %s Тихие часы Начало Окончание @@ -281,6 +292,9 @@ Отправить ещё раз URL файла Ссылка скопирована в буфер обмена + XMPP-адрес скопирован в буфер обмена + Сообщение об ошибке скопировано в буфер обмена + веб-адрес Сканировать 2D штрихкод Показать 2D штрихкод Показать чёрный список @@ -289,6 +303,14 @@ Повторить Оставить службу на переднем плане Не позволяет операционной системе закрыть ваше соединение + Создать резервную копию + Файлы резервной копии будут сохранены в %s + Создание резервной копии + Ваша резервная копия была создана + Файлы резервной копии сохранены в %s + Восстановление из резервной копии + Восстановление из резервной копии выполнено + Не забудьте включить аккаунт Выбрать файл %1$s загружается (%2$d%% выполнено) Загрузить %s @@ -311,7 +333,7 @@ Не удалось создать конференцию! Аватар аккаунта Скопировать OMEMO-отпечаток в буфер обмена - Создать заново ключ OMEMO + Создать ключ OMEMO заново Очистить устройства Вы уверены, что хотите очистить все остальные устройства из анонса ключей OMEMO? При соединении устройств в следующий раз новые ключи анонсируются автоматически, но устройства могут не получить сообщения, посланные до этого. Для этого контакта не существует доступных ключей.\nПопытка получения новых ключей с сервера оказалась неудачной. Возможно, что-то не так с сервером вашего собеседника. @@ -366,8 +388,8 @@ Скрыть пользователей вне сети %s печатает… %s прекратил набор - %s набирает... - %s перестал печатать + %s печатают... + %s перестали печатать Оповещения о наборе Позволяет вашим контактам видеть когда вы пишете им новое сообщение Отправить местоположение @@ -395,7 +417,8 @@ Последнее выбранное Выбрать быстрое действие Поиск контактов - Отправить частное сообщение + Поиск закладок + Отправить личное сообщение %1$s покинул конференцию! Имя пользователя Имя пользователя @@ -409,8 +432,8 @@ Сервер не ответственен за домен Повреждено Доступность - Вышел когда экран выключен - Отмечает ваш ресурс как «вышел» когда экран выключен + \"Отошёл\" когда экран выключен + Отмечает ваш ресурс как «отошёл» когда экран выключен \"Не беспокоить\" в беззвучном режиме Помечать ресурс как \"Не беспокоить\", когда устройство в беззвучном режиме Не доступен в режиме вибрации @@ -430,7 +453,7 @@ Цепочка сертификата не доверена Обновить сертификат Ошибка при получении OMEMO ключа! - Проверен OMEMO ключ с сертификатом! + Ключ OMEMO проверен с сертификатом! Ваше устройство не поддерживает выбор клиентских сертификатов! Подключение Соединение через Tor @@ -459,6 +482,8 @@ Уведомлять только при упоминании Без уведомления Уведомления приостановлены + Сжатие изображений + Изменять размер и сжимать изображения Всегда Автоматически Оптимизации энергопотребления разрешены @@ -475,9 +500,12 @@ Ошибка безопасности: недействительный доступ к файлу Не найдено приложения для отправки Отправить URI… + Ваш полный XMPP-адрес будет: %s Создать аккаунт Использовать свой провайдер Выберите имя пользователя + Управлять доступностью вручную + Устанавливать свою доступность при редактировании статусного сообщения Статусное собщение Свободен для общения В сети @@ -491,6 +519,7 @@ Выбрать участников Создание конференции… Пригласить ещё раз + Выключен Короткий Средний Длинный @@ -506,7 +535,7 @@ Компьютер Телефон Планшет - Веб браузер + Веб-браузер Консоль Требуется оплата Доступ в интернет запрещён @@ -548,10 +577,10 @@ Очистить закрытое хранилище, где хранятся файлы (Файлы можно заново скачать с сервера) Открывать ссылки из надёжного источника Вы потвердите OMEMO ключи %1$s после нажатия на ссылку. Это безопасно только если вы перешли по ссылке из доверенного источника, где только %2$sмог разместить эту ссылку. - Проверить OMEMO ключ + Проверить OMEMO-ключи Показывать неактивные Скрыть неактивные - Не доверенное устройство. + Прекратить доверять устройству Вы действительно хотите удалить устройство из доверенных?\Устройство и сообщения, полученные с этого устройства, будут помечаться как недоверенные. %d секунда @@ -618,7 +647,10 @@ Скопировать в буфер обмена Сообщение скопировано в буфер обмена Сообщение + Личные сообщения выключены Принять Неизвестный Сертификат? + Прокручивать вниз + Прокручивать вниз после отправки сообщения Редактировать статусное сообщение Редактировать статусное сообщение Отключить шифрование @@ -626,16 +658,43 @@ Отключить сейчас Черновик: OMEMO-шифрование + OMEMO будет всегда использоваться для одиночных бесед и закрытых конференций. + OMEMO будет использоваться по умолчанию для новых бесед. + OMEMO нужно будет явно включать для новых бесед. Размер шрифта + Относительный размер шрифта используемый в приложении. + Включено по умолчанию + Выключено по умолчанию Маленький Средний Большой + Сообщение не зашифровано для этого устройства. + Не удалось расшифровать OMEMO-сообщение. отменить Копировать местоположение Поделиться местоположением Поделиться местоположением Показать местоположение Поделиться + Невозможно начать запись Пожалуйста, подождите… + Conversations нужен доступ к микрофону Поиск сообщений + Посмотреть беседу + Копировать веб-адрес + Копировать XMPP-адрес + Быстрый поиск + На экране \"Начать беседу\" открывать клавиатуру и ставить курсор в поле поиска + Настройки уведомлений + Просмотр медиа + Качество видео + Низкое качество означает меньшие файлы + Среднее (360p) + Высокое (720р) + Оригинал (без сжатия) + Создать конференцию + Присоединиться к каналу + Создать закрытую конференцию + Создать публичный канал + Найти каналы diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 2e2964bf8..b095751f6 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -17,6 +17,8 @@ Unblock contact Block domain Unblock domain + Block participant + Unblock participant Manage Accounts Settings Share with Conversation diff --git a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java index e85898688..2a3d1a648 100644 --- a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java +++ b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java @@ -5,13 +5,16 @@ import android.util.Log; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.stanzas.IqPacket; @@ -19,82 +22,161 @@ import rocks.xmpp.addr.Jid; public class PushManagementService { - protected final XmppConnectionService mXmppConnectionService; + protected final XmppConnectionService mXmppConnectionService; - PushManagementService(XmppConnectionService service) { - this.mXmppConnectionService = service; - } + PushManagementService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } - void registerPushTokenOnServer(final Account account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support"); - retrieveFcmInstanceToken(token -> { - final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); - final Jid appServer = Jid.of(mXmppConnectionService.getString(R.string.app_server)); - IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(appServer, token, androidId); - mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> { - Element command = p.findChild("command", "http://jabber.org/protocol/commands"); - if (p.getType() == IqPacket.TYPE.RESULT && command != null) { - Element x = command.findChild("x", Namespace.DATA); - if (x != null) { - Data data = Data.parse(x); - try { - String node = data.getValue("node"); - String secret = data.getValue("secret"); - Jid jid = Jid.of(data.getValue("jid")); - if (node != null && secret != null) { - enablePushOnServer(a, jid, node, secret); - } - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - } - } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server"); - } - }); - }); - } + private static Data findResponseData(IqPacket response) { + final Element command = response.findChild("command", Namespace.COMMANDS); + final Element x = command == null ? null : command.findChild("x", Namespace.DATA); + return x == null ? null : Data.parse(x); + } - private void enablePushOnServer(final Account account, final Jid jid, final String node, final String secret) { - IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(jid, node, secret); - mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> { - if (p.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on server"); - } else if (p.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on server failed"); - } - }); - } + private Jid getAppServer() { + return Jid.of(mXmppConnectionService.getString(R.string.app_server)); + } - private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) { - new Thread(() -> { - try { - instanceTokenRetrieved.onGcmInstanceTokenRetrieved(FirebaseInstanceId.getInstance().getToken()); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to get push token",e); - } - }).start(); + void registerPushTokenOnServer(final Account account) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support"); + retrieveFcmInstanceToken(token -> { + final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); + final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId); + mXmppConnectionService.sendIqPacket(account, packet, (a, response) -> { + final Data data = findResponseData(response); + if (response.getType() == IqPacket.TYPE.RESULT && data != null) { + try { + String node = data.getValue("node"); + String secret = data.getValue("secret"); + Jid jid = Jid.of(data.getValue("jid")); + if (node != null && secret != null) { + enablePushOnServer(a, jid, node, secret); + } + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + } else { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server"); + } + }); + }); + } - } + public void unregisterChannel(final Account account, final String channel) { + final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); + final Jid appServer = getAppServer(); + final IqPacket packet = mXmppConnectionService.getIqGenerator().unregisterChannelOnAppServer(appServer, androidId, channel); + mXmppConnectionService.sendIqPacket(account, packet, (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG,a.getJid().asBareJid()+": successfully unregistered channel"); + } else if (response.getType() == IqPacket.TYPE.ERROR) { + Log.d(Config.LOGTAG, a.getJid().asBareJid()+": unable to unregister channel with hash "+channel); + } + }); + } + + void registerPushTokenOnServer(final Conversation conversation) { + Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": room "+conversation.getJid().asBareJid()+" has push support"); + retrieveFcmInstanceToken(token -> { + final Jid muc = conversation.getJid().asBareJid(); + final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); + final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId, muc); + packet.setTo(muc); + mXmppConnectionService.sendIqPacket(conversation.getAccount(), packet, (a, response) -> { + final Data data = findResponseData(response); + if (response.getType() == IqPacket.TYPE.RESULT && data != null) { + try { + final String node = data.getValue("node"); + final String secret = data.getValue("secret"); + final Jid jid = Jid.of(data.getValue("jid")); + if (node != null && secret != null) { + enablePushOnServer(conversation, jid, node, secret); + } + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + } else { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server"); + } + }); + }); + } + + private void enablePushOnServer(final Account account, final Jid appServer, final String node, final String secret) { + final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); + mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> { + if (p.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on server"); + } else if (p.getType() == IqPacket.TYPE.ERROR) { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on server failed"); + } + }); + } + + private void enablePushOnServer(final Conversation conversation, final Jid appServer, final String node, final String secret) { + final Jid muc = conversation.getJid().asBareJid(); + final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); + enable.setTo(muc); + mXmppConnectionService.sendIqPacket(conversation.getAccount(), enable, (a, p) -> { + if (p.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on " + muc); + if (conversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, node)) { + mXmppConnectionService.updateConversation(conversation); + } + } else if (p.getType() == IqPacket.TYPE.ERROR) { + Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on " + muc + " failed"); + } + }); + } + + public void disablePushOnServer(final Conversation conversation) { + final Jid muc = conversation.getJid().asBareJid(); + final String node = conversation.getAttribute(Conversation.ATTRIBUTE_PUSH_NODE); + if (node != null) { + final IqPacket disable = mXmppConnectionService.getIqGenerator().disablePush(getAppServer(), node); + disable.setTo(muc); + mXmppConnectionService.sendIqPacket(conversation.getAccount(), disable, (account, response) -> { + if (response.getType() == IqPacket.TYPE.ERROR) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to disable push for room "+muc); + } + }); + } else { + Log.d(Config.LOGTAG,conversation.getAccount().getJid().asBareJid()+": room "+muc+" has no stored node. unable to disable push"); + } + } + + private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) { + FirebaseInstanceId.getInstance().getInstanceId().addOnCompleteListener(task -> { + if (!task.isSuccessful()) { + Log.d(Config.LOGTAG, "unable to get Firebase instance token", task.getException()); + } + final InstanceIdResult result = task.getResult(); + if (result != null) { + instanceTokenRetrieved.onGcmInstanceTokenRetrieved(result.getToken()); + } + }); + + } - public boolean available(Account account) { - final XmppConnection connection = account.getXmppConnection(); - return connection != null - && connection.getFeatures().sm() - && connection.getFeatures().push() - && playServicesAvailable(); - } + public boolean available(Account account) { + final XmppConnection connection = account.getXmppConnection(); + return connection != null + && connection.getFeatures().sm() + && connection.getFeatures().push() + && playServicesAvailable(); + } - private boolean playServicesAvailable() { - return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS; - } + private boolean playServicesAvailable() { + return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS; + } - public boolean isStub() { - return false; - } + public boolean isStub() { + return false; + } - interface OnGcmInstanceTokenRetrieved { - void onGcmInstanceTokenRetrieved(String token); - } + interface OnGcmInstanceTokenRetrieved { + void onGcmInstanceTokenRetrieved(String token); + } } diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java index d5db6d436..cfa2f7775 100644 --- a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -203,7 +203,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService os.close(); connection.connect(); final int code = connection.getResponseCode(); - if (code == 200) { + if (code == 200 || code == 201) { account.setOption(Account.OPTION_UNVERIFIED, false); account.setOption(Account.OPTION_DISABLED, false); awaitingAccountStateChange = new CountDownLatch(1); diff --git a/src/quicksy/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java b/src/quicksy/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java index faab325d8..d1f987bff 100644 --- a/src/quicksy/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java +++ b/src/quicksy/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java @@ -42,6 +42,9 @@ public class ApiDialogHelper { case 409: res = R.string.logged_in_with_another_device; break; + case 451: + res = R.string.not_available_in_your_country; + break; case 500: res = R.string.something_went_wrong_processing_your_request; break; diff --git a/src/quicksy/res/values-ar/strings.xml b/src/quicksy/res/values-ar/strings.xml index a6fc2fa5b..b3b1a2755 100644 --- a/src/quicksy/res/values-ar/strings.xml +++ b/src/quicksy/res/values-ar/strings.xml @@ -6,4 +6,4 @@ إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي كويكسي يحتاج الإتصال بالمايكروفون صورة حساب كويكسي - + diff --git a/src/quicksy/res/values-bg/strings.xml b/src/quicksy/res/values-bg/strings.xml index 5e99a26f2..1ec1e155d 100644 --- a/src/quicksy/res/values-bg/strings.xml +++ b/src/quicksy/res/values-bg/strings.xml @@ -19,4 +19,5 @@ Quicksy се нуждае от достъп до микрофона Тази категория известия се използва за показване на постоянно известие, което показва, че Quicksy работи. Профилна снимка за Quicksy + Quicksy не може да се използва във Вашата страна. diff --git a/src/quicksy/res/values-ca/strings.xml b/src/quicksy/res/values-ca/strings.xml index bce36c170..ccde5a832 100644 --- a/src/quicksy/res/values-ca/strings.xml +++ b/src/quicksy/res/values-ca/strings.xml @@ -19,4 +19,4 @@ Quicksy necessita accés al micròfon Aquest tipus de notificació s\'utilitza per mostrar una notificació permanent que indica que Quicksy s\'està executant. Imatge de perfil en Quicksy - + diff --git a/src/quicksy/res/values-de/strings.xml b/src/quicksy/res/values-de/strings.xml index 0f6f71366..9fed2e699 100644 --- a/src/quicksy/res/values-de/strings.xml +++ b/src/quicksy/res/values-de/strings.xml @@ -19,4 +19,5 @@ Quicksy benötigt Zugriff auf das Mikrofon Diese Benachrichtigungsart wird verwendet, um eine permanente Benachrichtigung anzuzeigen, die anzeigt, dass Quicksy gerade ausgeführt wird. Quicksy Profilbild + Quicksy ist in deinem Land nicht verfügbar. diff --git a/src/quicksy/res/values-es/strings.xml b/src/quicksy/res/values-es/strings.xml index 02ea53c82..bb9c8d206 100644 --- a/src/quicksy/res/values-es/strings.xml +++ b/src/quicksy/res/values-es/strings.xml @@ -19,4 +19,4 @@ Quicksy necesita acceder al micrófono Esta categoría de notificación se usa para mostrar una notificación permantente indicando que Quicksy está ejecutándose. Foto de perfil en Quicksy - + diff --git a/src/quicksy/res/values-fr/strings.xml b/src/quicksy/res/values-fr/strings.xml index 4dae4f7f4..d86768e1d 100644 --- a/src/quicksy/res/values-fr/strings.xml +++ b/src/quicksy/res/values-fr/strings.xml @@ -19,4 +19,4 @@ Quicksy doit avoir accès au microphone Cette catégorie de notification est utilisée pour afficher une notification permanente indiquant que Quicksy est en cours d\'exécution. Photo de profil Quicksy - + diff --git a/src/quicksy/res/values-gl/strings.xml b/src/quicksy/res/values-gl/strings.xml index 826b3c931..c17eb3bfc 100644 --- a/src/quicksy/res/values-gl/strings.xml +++ b/src/quicksy/res/values-gl/strings.xml @@ -19,4 +19,5 @@ Quicksy precisa acceder ao micrófono Esta categoría de notificacións utilízase para mostrar unha notificación permanente que indica que Quicksy está funcionando. Imaxe de perfil Quicksy + Quicksy non está dispoñible no seu país. diff --git a/src/quicksy/res/values-hu/strings.xml b/src/quicksy/res/values-hu/strings.xml index e4c753841..af2244f39 100644 --- a/src/quicksy/res/values-hu/strings.xml +++ b/src/quicksy/res/values-hu/strings.xml @@ -19,4 +19,4 @@ A Quicksy-nek hozzáférésre lenne szüksége a mikrofonhoz Ez az értesítési kategória állandó értesítést jelenít meg arról, hogy a Quicksy fut. Quicksy profilkép - + diff --git a/src/quicksy/res/values-it/strings.xml b/src/quicksy/res/values-it/strings.xml index 5438621bc..4d61c3c6b 100644 --- a/src/quicksy/res/values-it/strings.xml +++ b/src/quicksy/res/values-it/strings.xml @@ -19,4 +19,4 @@ Quicksy ha bisogno di accedere al microfono Questa categoria di notifiche è usata per mostrare una notifica permanente per indicare che Quicksy è in esecuzione. Immagine profilo di Quicksy - + diff --git a/src/quicksy/res/values-ja/strings.xml b/src/quicksy/res/values-ja/strings.xml index 282d11d7a..27adfeeb6 100644 --- a/src/quicksy/res/values-ja/strings.xml +++ b/src/quicksy/res/values-ja/strings.xml @@ -19,4 +19,4 @@ Quicksy はマイクにアクセスが必要です この通知カテゴリーは Quicksy が実行されていることを表示する、永続的な通知を表示するために使用されます。 Quicksy プロフィール写真 - + diff --git a/src/quicksy/res/values-nl/strings.xml b/src/quicksy/res/values-nl/strings.xml index 7c149ad2a..a34babb9d 100644 --- a/src/quicksy/res/values-nl/strings.xml +++ b/src/quicksy/res/values-nl/strings.xml @@ -19,4 +19,4 @@ Quicksy heeft toegang nodig tot de microfoon Deze meldingscategorie wordt gebruikt om een permanente melding weer te geven dat Quicksy wordt uitgevoerd. Quicksy-profielafbeelding - + diff --git a/src/quicksy/res/values-pl/strings.xml b/src/quicksy/res/values-pl/strings.xml index 10ee5a003..b08a73a90 100644 --- a/src/quicksy/res/values-pl/strings.xml +++ b/src/quicksy/res/values-pl/strings.xml @@ -19,4 +19,4 @@ Quicksy potrzebuje dostępu do mikrofonu. Ta kategoria powiadomień jest używana do wyświetlania ciągłego powiadomienia o tym, że Quicksy działa. Obrazek profilowy Quicksy - + diff --git a/src/quicksy/res/values-pt-rBR/strings.xml b/src/quicksy/res/values-pt-rBR/strings.xml index dc3a00e4b..6e78e69fd 100644 --- a/src/quicksy/res/values-pt-rBR/strings.xml +++ b/src/quicksy/res/values-pt-rBR/strings.xml @@ -19,4 +19,4 @@ O Quicksy necessita de acesso ao microfone Essa categoria de notificação é utilizada para exibir uma notificação permanente indicando que o Quicksy está em execução. Imagem de perfil do Quicksy - + diff --git a/src/quicksy/res/values-ro-rRO/strings.xml b/src/quicksy/res/values-ro-rRO/strings.xml index 6f268ab72..bc6ca871f 100644 --- a/src/quicksy/res/values-ro-rRO/strings.xml +++ b/src/quicksy/res/values-ro-rRO/strings.xml @@ -21,4 +21,5 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Quicksy are nevoie de acces la microfon Această categorie de notificări este folosită pentru a arăta o notificare permanentă ce indică rularea Quicksy Poză profil Quicksy + Quicksy nu este disponibilă în țara dumneavoastră. diff --git a/src/quicksy/res/values-uk/strings.xml b/src/quicksy/res/values-uk/strings.xml index ea10e6f82..ab585b8a2 100644 --- a/src/quicksy/res/values-uk/strings.xml +++ b/src/quicksy/res/values-uk/strings.xml @@ -19,4 +19,4 @@ Програма потребує доступу до мікрофона Цей вид сповіщень показує постійне сповіщення про те, що ця програма працює. Зображення профілю для Quicksy - + diff --git a/src/quicksy/res/values-zh-rCN/strings.xml b/src/quicksy/res/values-zh-rCN/strings.xml index 24010d97f..b2830539d 100644 --- a/src/quicksy/res/values-zh-rCN/strings.xml +++ b/src/quicksy/res/values-zh-rCN/strings.xml @@ -19,4 +19,4 @@ Quicksy需要麦克风权限 此通知类别用于显示表明Quicksy正在运行的永久通知。 Quicksy个人资料图片 - + diff --git a/src/quicksy/res/values/strings.xml b/src/quicksy/res/values/strings.xml index 3792c2ada..e9cfa3cae 100644 --- a/src/quicksy/res/values/strings.xml +++ b/src/quicksy/res/values/strings.xml @@ -19,4 +19,5 @@ Quicksy needs access to the microphone This notification category is used to display a permanent notification indicating that Quicksy is running. Quicksy profile picture + Quicksy is not available in your country.