Compare commits

...

26 Commits

Author SHA1 Message Date
Martin/Geno b459a4c6e3
add metadata/F-Droid changelog 2019-07-15 19:04:01 +02:00
Martin/Geno 415a105b41
Merge tag '2.5.4' into develop 2019-07-15 19:02:29 +02:00
Daniel Gultsch b58d011737 version bump to 2.5.4 + changelog 2019-07-13 08:46:41 +02:00
Daniel Gultsch 553b65ffcd pulled translations from transifex 2019-07-11 08:14:07 +02:00
Daniel Gultsch 8c654abff6 updated retrofit 2019-07-10 22:10:36 +02:00
Daniel Gultsch 7e93c1021b handle blocking and unblocking of full jids 2019-07-10 17:58:48 +02:00
Daniel Gultsch c9bf1474e3 support status code 451 in quicksy registration 2019-07-04 19:35:03 +02:00
Daniel Gultsch 2956cfdb95 downgrade some deps that require androidX 2019-07-04 19:34:15 +02:00
Daniel Gultsch fe0493d93f android libphonenumber not yet up to date with upstream 2019-07-04 18:27:07 +02:00
Daniel Gultsch 8138eb0346 use more default values in LocationActivity
fixes #3475
2019-07-04 18:17:16 +02:00
Daniel Gultsch f3ab2dd33a updated some dependencies 2019-07-04 18:16:39 +02:00
Daniel Gultsch 98c4e9056f use helper method to close socket 2019-07-04 10:12:08 +02:00
Daniel Gultsch e9099acd97 accept status code 201 for Quicksy registration 2019-07-03 18:01:46 +02:00
Daniel Gultsch feba9a71ee code clean up 2019-07-02 11:10:21 +02:00
Daniel Gultsch 8c526de0af disable muc push on archive instead of leave
leave can be triggered on swipe and doesn’t mean we don’t want pushes
2019-07-01 14:35:00 +02:00
Daniel Gultsch 5304ac60a7 version bump to 2.5.4-beta 2019-07-01 14:34:13 +02:00
Daniel Gultsch 59a2f39b27 pulled translations from transifex 2019-07-01 11:17:27 +02:00
Daniel Gultsch 4f0214b477 check if activity is not null before using it to paint send button 2019-07-01 10:17:29 +02:00
Daniel Gultsch 7ec8f7952f migrate copy ond write list to synchronized hashset for pending mucs 2019-06-30 21:57:37 +02:00
Daniel Gultsch 9f08a32ffb include remote server errors in errors that should trigger a self ping 2019-06-30 20:08:28 +02:00
Daniel Gultsch 0ecdb43be6 rate limit muc pings / joins. never run two pings at same time 2019-06-30 19:54:07 +02:00
Daniel Gultsch 49224335fc attempt to unregister when receiving push for channel no longer joined
when receiving a FCM push message for a channel the user is no longer in (this can happen when the disable command failed) an attempt will be made to explicitly unregister from the app server (which in turn will then send item-not-found on next push)
2019-06-26 17:40:12 +02:00
Daniel Gultsch 7809af9b57 implement FCM push for group chats 2019-06-25 18:15:51 +02:00
Daniel Gultsch e467fe341e implement client support for muc push
Staying connected to a MUC room hosted on a remote server can be challenging.

If a server reboots it will usually send a shut down notification to all
participants. However even if a client knows that a server was shut down it
doesn’t know when it comes up again. In some corner cases that shut down
notification might not even be delivered successfully leaving the client in a
state where it thinks it is connected but it really isn’t.

The possible work around implemented in this commit is to register the clients
full JID (user@domain.tld/Conversations.r4nd) as an App Server according to
XEP-0357 with the room. (Conversations checks for the push:0 namespace on the
room.)

After cycling through a reboot the first message send to a room will trigger
pubsub notifications to each registered full JID. This event will be used to
trigger a XEP-0410 ping and if necessary a subsequent rejoin of the MUC.

If the resource has become unavailable during down time of the MUC server the
user’s server will respond with an IQ error which in turn leads to the MUC
server disabling that push target.

Leaving a MUC will send a `disable` command. If sending that disable command
failed for some reason (network outage) and the client receives a pubsub
notification for a room it is no longer joined in it will respond with an
item-not-found IQ error which also disables subsequent pushes from the server.

Note: We 0410-ping before a join to avoid unnecessary full joins which can be
quite costly. Further client side optimazations will also surpress pings when
a ping is already in flight to further save traffic.
2019-06-24 18:16:06 +02:00
Daniel Gultsch b6d059ed89 ping muc after receiving not-acceptable error 2019-06-18 18:40:16 +02:00
Daniel Gultsch 17c8bf3452 attempt to keep messages waiting until muc is connected 2019-06-18 18:09:44 +02:00
58 changed files with 1080 additions and 563 deletions

View File

@ -1,5 +1,8 @@
# Changelog # Changelog
### Version 2.5.4
* stability improvements for group chats and channels
### Version 2.5.3 ### Version 2.5.3
* bug fixes for peer to peer file transfer (Jingle) * bug fixes for peer to peer file transfer (Jingle)
* fixed server info for unlimited/unknown max file size * fixed server info for unlimited/unknown max file size

View File

@ -51,7 +51,7 @@ dependencies {
conversationsFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion" conversationsFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion"
quicksyFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion" quicksyFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion"
implementation 'org.bouncycastle:bcmail-jdk15on:1.58' 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 'de.measite.minidns:minidns-hla:0.2.4'
implementation 'me.leolin:ShortcutBadger:1.1.22@aar' implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
implementation 'org.whispersystems:signal-protocol-java:2.6.2' implementation 'org.whispersystems:signal-protocol-java:2.6.2'
@ -59,13 +59,13 @@ dependencies {
implementation "com.wefika:flowlayout:0.4.1" implementation "com.wefika:flowlayout:0.4.1"
implementation 'net.ypresto.androidtranscoder:android-transcoder:0.3.0' implementation 'net.ypresto.androidtranscoder:android-transcoder:0.3.0'
implementation project(':libs:xmpp-addr') 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.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 'me.drakeet.support:toastcompat:1.1.0'
implementation "com.leinardi.android:speed-dial:2.0.1" implementation "com.leinardi.android:speed-dial:2.0.1"
implementation 'com.squareup.retrofit2:retrofit:2.5.0' implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0' implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation 'com.google.guava:guava:27.1-android' implementation 'com.google.guava:guava:27.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.10.1' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.10.1'
} }
@ -81,8 +81,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 25 targetSdkVersion 25
versionCode 330 versionCode 333
versionName "2.5.3" versionName "2.5.4"
archivesBaseName += "-$versionName" archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations" applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId resValue "string", "applicationId", applicationId

View File

@ -0,0 +1,2 @@
* bug fixes for peer to peer file transfer (Jingle)
* fixed server info for unlimited/unknown max file size

View File

@ -0,0 +1 @@
* stability improvements for group chats and channels

31
proguard-rules.pro vendored
View File

@ -20,3 +20,34 @@
-dontwarn com.google.firebase.analytics.connector.AnalyticsConnector -dontwarn com.google.firebase.analytics.connector.AnalyticsConnector
-dontwarn java.lang.** -dontwarn java.lang.**
-dontwarn javax.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.* <methods>;
}
# 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.* <methods>; }
-keep,allowobfuscation interface <1>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Выберите своего XMPP-провайдера</string>
<string name="use_conversations.im">Использовать conversations.im</string>
<string name="create_new_account">Создать новый аккаунт</string>
</resources>

View File

@ -1,6 +1,7 @@
package eu.siacs.conversations.services; package eu.siacs.conversations.services;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
public class PushManagementService { public class PushManagementService {
@ -10,7 +11,19 @@ public class PushManagementService {
this.mXmppConnectionService = service; 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 //stub implementation. only affects playstore flavor
} }

View File

@ -2,7 +2,6 @@ package eu.siacs.conversations.crypto;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.os.Parcelable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.util.Log; import android.util.Log;
@ -94,7 +93,7 @@ public class PgpEngine {
break; break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
break; break;
case OpenPgpApi.RESULT_CODE_ERROR: case OpenPgpApi.RESULT_CODE_ERROR:
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
@ -133,7 +132,7 @@ public class PgpEngine {
callback.success(message); callback.success(message);
break; break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
break; break;
case OpenPgpApi.RESULT_CODE_ERROR: case OpenPgpApi.RESULT_CODE_ERROR:
logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
@ -200,7 +199,7 @@ public class PgpEngine {
callback.success(account); callback.success(account);
return; return;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account); callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account);
return; return;
case OpenPgpApi.RESULT_CODE_ERROR: case OpenPgpApi.RESULT_CODE_ERROR:
logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
@ -249,7 +248,7 @@ public class PgpEngine {
callback.success(signatureBuilder.toString()); callback.success(signatureBuilder.toString());
return; return;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status); callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status);
return; return;
case OpenPgpApi.RESULT_CODE_ERROR: case OpenPgpApi.RESULT_CODE_ERROR:
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
@ -276,7 +275,7 @@ public class PgpEngine {
callback.success(contact); callback.success(contact);
return; return;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact); callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact);
return; return;
case OpenPgpApi.RESULT_CODE_ERROR: case OpenPgpApi.RESULT_CODE_ERROR:
logError(contact.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); logError(contact.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));

View File

@ -64,8 +64,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
protected final JSONObject keys; protected final JSONObject keys;
private final Roster roster = new Roster(this); private final Roster roster = new Roster(this);
private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>(); private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>(); public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>(); public final Set<Conversation> pendingConferenceLeaves = new HashSet<>();
public final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
public final Set<Conversation> inProgressConferencePings = new HashSet<>();
protected Jid jid; protected Jid jid;
protected String password; protected String password;
protected int options = 0; protected int options = 0;

View File

@ -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_MUTED_TILL = "muted_till";
public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; 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"; public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message"; private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";

View File

@ -17,6 +17,7 @@ import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.utils.JidHelper; import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.forms.Field; import eu.siacs.conversations.xmpp.forms.Field;
@ -113,6 +114,10 @@ public class MucOptions {
return MessageArchiveService.Version.has(getFeatures()); return MessageArchiveService.Version.has(getFeatures());
} }
public boolean push() {
return getFeatures().contains(Namespace.PUSH);
}
public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) { public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) {
this.serviceDiscoveryResult = serviceDiscoveryResult; this.serviceDiscoveryResult = serviceDiscoveryResult;
String name; String name;

View File

@ -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<Tag> 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());
}
}

View File

@ -305,7 +305,7 @@ public class IqGenerator extends AbstractGenerator {
public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) { public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) {
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
final Element block = iq.addChild("block", Namespace.BLOCKING); 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) { if (reportSpam) {
item.addChild("report", "urn:xmpp:reporting:0").addChild("spam"); item.addChild("report", "urn:xmpp:reporting:0").addChild("spam");
} }
@ -316,7 +316,7 @@ public class IqGenerator extends AbstractGenerator {
public IqPacket generateSetUnblockRequest(final Jid jid) { public IqPacket generateSetUnblockRequest(final Jid jid) {
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
final Element block = iq.addChild("unblock", Namespace.BLOCKING); 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; return iq;
} }
@ -423,29 +423,60 @@ public class IqGenerator extends AbstractGenerator {
} }
public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) { 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); 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("node", "register-push-fcm");
command.setAttribute("action", "execute"); command.setAttribute("action", "execute");
Data data = new Data(); final Data data = new Data();
data.put("token", token); data.put("token", token);
data.put("android-id", deviceId); 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(); data.submit();
command.addChild(data); command.addChild(data);
return packet; 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); 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("jid", jid.toString());
enable.setAttribute("node", node); enable.setAttribute("node", node);
Data data = new Data(); if (secret != null) {
data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS); Data data = new Data();
data.put("secret", secret); data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS);
data.submit(); data.put("secret", secret);
enable.addChild(data); 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; return packet;
} }

View File

@ -26,6 +26,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
@ -37,360 +38,383 @@ import rocks.xmpp.addr.Jid;
public class IqParser extends AbstractParser implements OnIqPacketReceived { public class IqParser extends AbstractParser implements OnIqPacketReceived {
public IqParser(final XmppConnectionService service) { public IqParser(final XmppConnectionService service) {
super(service); super(service);
} }
private void rosterItems(final Account account, final Element query) { private void rosterItems(final Account account, final Element query) {
final String version = query.getAttribute("ver"); final String version = query.getAttribute("ver");
if (version != null) { if (version != null) {
account.getRoster().setVersion(version); account.getRoster().setVersion(version);
} }
for (final Element item : query.getChildren()) { for (final Element item : query.getChildren()) {
if (item.getName().equals("item")) { if (item.getName().equals("item")) {
final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
if (jid == null) { if (jid == null) {
continue; continue;
} }
final String name = item.getAttribute("name"); final String name = item.getAttribute("name");
final String subscription = item.getAttribute("subscription"); final String subscription = item.getAttribute("subscription");
final Contact contact = account.getRoster().getContact(jid); final Contact contact = account.getRoster().getContact(jid);
boolean bothPre = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM); boolean bothPre = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
contact.setServerName(name); contact.setServerName(name);
contact.parseGroupsFromElement(item); contact.parseGroupsFromElement(item);
} }
if ("remove".equals(subscription)) { if ("remove".equals(subscription)) {
contact.resetOption(Contact.Options.IN_ROSTER); contact.resetOption(Contact.Options.IN_ROSTER);
contact.resetOption(Contact.Options.DIRTY_DELETE); contact.resetOption(Contact.Options.DIRTY_DELETE);
contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
} else { } else {
contact.setOption(Contact.Options.IN_ROSTER); contact.setOption(Contact.Options.IN_ROSTER);
contact.resetOption(Contact.Options.DIRTY_PUSH); contact.resetOption(Contact.Options.DIRTY_PUSH);
contact.parseSubscriptionFromElement(item); contact.parseSubscriptionFromElement(item);
} }
boolean both = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM); boolean both = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
if ((both != bothPre) && both) { if ((both != bothPre) && both) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": gained mutual presence subscription with "+contact.getJid()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": gained mutual presence subscription with " + contact.getJid());
AxolotlService axolotlService = account.getAxolotlService(); AxolotlService axolotlService = account.getAxolotlService();
if (axolotlService != null) { if (axolotlService != null) {
axolotlService.clearErrorsInFetchStatusMap(contact.getJid()); axolotlService.clearErrorsInFetchStatusMap(contact.getJid());
} }
} }
mXmppConnectionService.getAvatarService().clear(contact); mXmppConnectionService.getAvatarService().clear(contact);
} }
} }
mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateRosterUi(); mXmppConnectionService.updateRosterUi();
mXmppConnectionService.getShortcutService().refresh(); mXmppConnectionService.getShortcutService().refresh();
mXmppConnectionService.syncRoster(account); mXmppConnectionService.syncRoster(account);
} }
public String avatarData(final IqPacket packet) { public String avatarData(final IqPacket packet) {
final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
if (pubsub == null) { if (pubsub == null) {
return null; return null;
} }
final Element items = pubsub.findChild("items"); final Element items = pubsub.findChild("items");
if (items == null) { if (items == null) {
return null; return null;
} }
return super.avatarData(items); return super.avatarData(items);
} }
public Element getItem(final IqPacket packet) { public Element getItem(final IqPacket packet) {
final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
if (pubsub == null) { if (pubsub == null) {
return null; return null;
} }
final Element items = pubsub.findChild("items"); final Element items = pubsub.findChild("items");
if (items == null) { if (items == null) {
return null; return null;
} }
return items.findChild("item"); return items.findChild("item");
} }
@NonNull @NonNull
public Set<Integer> deviceIds(final Element item) { public Set<Integer> deviceIds(final Element item) {
Set<Integer> deviceIds = new HashSet<>(); Set<Integer> deviceIds = new HashSet<>();
if (item != null) { if (item != null) {
final Element list = item.findChild("list"); final Element list = item.findChild("list");
if (list != null) { if (list != null) {
for (Element device : list.getChildren()) { for (Element device : list.getChildren()) {
if (!device.getName().equals("device")) { if (!device.getName().equals("device")) {
continue; continue;
} }
try { try {
Integer id = Integer.valueOf(device.getAttribute("id")); Integer id = Integer.valueOf(device.getAttribute("id"));
deviceIds.add(id); deviceIds.add(id);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered invalid <device> node in PEP ("+e.getMessage()+"):" + device.toString()+ ", skipping..."); Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Encountered invalid <device> node in PEP (" + e.getMessage() + "):" + device.toString() + ", skipping...");
continue; continue;
} }
} }
} }
} }
return deviceIds; return deviceIds;
} }
public Integer signedPreKeyId(final Element bundle) { public Integer signedPreKeyId(final Element bundle) {
final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
if(signedPreKeyPublic == null) { if (signedPreKeyPublic == null) {
return null; return null;
} }
try { try {
return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId")); return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
return null; return null;
} }
} }
public ECPublicKey signedPreKeyPublic(final Element bundle) { public ECPublicKey signedPreKeyPublic(final Element bundle) {
ECPublicKey publicKey = null; ECPublicKey publicKey = null;
final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
if(signedPreKeyPublic == null) { if (signedPreKeyPublic == null) {
return null; return null;
} }
try { try {
publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0); publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(), Base64.DEFAULT), 0);
} catch (Throwable e) { } catch (Throwable e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage()); Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Invalid signedPreKeyPublic in PEP: " + e.getMessage());
} }
return publicKey; return publicKey;
} }
public byte[] signedPreKeySignature(final Element bundle) { public byte[] signedPreKeySignature(final Element bundle) {
final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature"); final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature");
if(signedPreKeySignature == null) { if (signedPreKeySignature == null) {
return null; return null;
} }
try { try {
return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT); return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT);
} catch (Throwable e) { } catch (Throwable e) {
Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : Invalid base64 in signedPreKeySignature"); Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : Invalid base64 in signedPreKeySignature");
return null; return null;
} }
} }
public IdentityKey identityKey(final Element bundle) { public IdentityKey identityKey(final Element bundle) {
IdentityKey identityKey = null; IdentityKey identityKey = null;
final Element identityKeyElement = bundle.findChild("identityKey"); final Element identityKeyElement = bundle.findChild("identityKey");
if(identityKeyElement == null) { if (identityKeyElement == null) {
return null; return null;
} }
try { try {
identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
} catch (Throwable e) { } catch (Throwable e) {
Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage()); Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Invalid identityKey in PEP: " + e.getMessage());
} }
return identityKey; return identityKey;
} }
public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) { public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>(); Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
Element item = getItem(packet); Element item = getItem(packet);
if (item == null) { if (item == null) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet); Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Couldn't find <item> in bundle IQ packet: " + packet);
return null; return null;
} }
final Element bundleElement = item.findChild("bundle"); final Element bundleElement = item.findChild("bundle");
if(bundleElement == null) { if (bundleElement == null) {
return null; return null;
} }
final Element prekeysElement = bundleElement.findChild("prekeys"); final Element prekeysElement = bundleElement.findChild("prekeys");
if(prekeysElement == null) { if (prekeysElement == null) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet); Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Couldn't find <prekeys> in bundle IQ packet: " + packet);
return null; return null;
} }
for(Element preKeyPublicElement : prekeysElement.getChildren()) { for (Element preKeyPublicElement : prekeysElement.getChildren()) {
if(!preKeyPublicElement.getName().equals("preKeyPublic")){ if (!preKeyPublicElement.getName().equals("preKeyPublic")) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement); Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
continue; continue;
} }
Integer preKeyId = null; Integer preKeyId = null;
try { try {
preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
final ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0); final ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
preKeyRecords.put(preKeyId, preKeyPublic); preKeyRecords.put(preKeyId, preKeyPublic);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"could not parse preKeyId from preKey "+preKeyPublicElement.toString()); Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "could not parse preKeyId from preKey " + preKeyPublicElement.toString());
} catch (Throwable e) { } catch (Throwable e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Invalid preKeyPublic (ID=" + preKeyId + ") in PEP: " + e.getMessage() + ", skipping...");
} }
} }
return preKeyRecords; return preKeyRecords;
} }
public Pair<X509Certificate[],byte[]> verification(final IqPacket packet) { public Pair<X509Certificate[], byte[]> verification(final IqPacket packet) {
Element item = getItem(packet); Element item = getItem(packet);
Element verification = item != null ? item.findChild("verification",AxolotlService.PEP_PREFIX) : null; Element verification = item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null;
Element chain = verification != null ? verification.findChild("chain") : null; Element chain = verification != null ? verification.findChild("chain") : null;
Element signature = verification != null ? verification.findChild("signature") : null; Element signature = verification != null ? verification.findChild("signature") : null;
if (chain != null && signature != null) { if (chain != null && signature != null) {
List<Element> certElements = chain.getChildren(); List<Element> certElements = chain.getChildren();
X509Certificate[] certificates = new X509Certificate[certElements.size()]; X509Certificate[] certificates = new X509Certificate[certElements.size()];
try { try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
int i = 0; int i = 0;
for(Element cert : certElements) { for (Element cert : certElements) {
certificates[i] = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.getContent(),Base64.DEFAULT))); certificates[i] = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.getContent(), Base64.DEFAULT)));
++i; ++i;
} }
return new Pair<>(certificates,Base64.decode(signature.getContent(),Base64.DEFAULT)); return new Pair<>(certificates, Base64.decode(signature.getContent(), Base64.DEFAULT));
} catch (CertificateException e) { } catch (CertificateException e) {
return null; return null;
} }
} else { } else {
return null; return null;
} }
} }
public PreKeyBundle bundle(final IqPacket bundle) { public PreKeyBundle bundle(final IqPacket bundle) {
Element bundleItem = getItem(bundle); Element bundleItem = getItem(bundle);
if(bundleItem == null) { if (bundleItem == null) {
return null; return null;
} }
final Element bundleElement = bundleItem.findChild("bundle"); final Element bundleElement = bundleItem.findChild("bundle");
if(bundleElement == null) { if (bundleElement == null) {
return null; return null;
} }
ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
Integer signedPreKeyId = signedPreKeyId(bundleElement); Integer signedPreKeyId = signedPreKeyId(bundleElement);
byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
IdentityKey identityKey = identityKey(bundleElement); IdentityKey identityKey = identityKey(bundleElement);
if(signedPreKeyId == null || signedPreKeyPublic == null || identityKey == null) { if (signedPreKeyId == null || signedPreKeyPublic == null || identityKey == null) {
return null; return null;
} }
return new PreKeyBundle(0, 0, 0, null, return new PreKeyBundle(0, 0, 0, null,
signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
} }
public List<PreKeyBundle> preKeys(final IqPacket preKeys) { public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
List<PreKeyBundle> bundles = new ArrayList<>(); List<PreKeyBundle> bundles = new ArrayList<>();
Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys); Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
if ( preKeyPublics != null) { if (preKeyPublics != null) {
for (Integer preKeyId : preKeyPublics.keySet()) { for (Integer preKeyId : preKeyPublics.keySet()) {
ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
0, null, null, null)); 0, null, null, null));
} }
} }
return bundles; return bundles;
} }
@Override @Override
public void onIqPacketReceived(final Account account, final IqPacket packet) { public void onIqPacketReceived(final Account account, final IqPacket packet) {
final boolean isGet = packet.getType() == IqPacket.TYPE.GET; final boolean isGet = packet.getType() == IqPacket.TYPE.GET;
if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) { if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) {
return; return;
} }
if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) { if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) {
final Element query = packet.findChild("query"); final Element query = packet.findChild("query");
// If this is in response to a query for the whole roster: // If this is in response to a query for the whole roster:
if (packet.getType() == IqPacket.TYPE.RESULT) { if (packet.getType() == IqPacket.TYPE.RESULT) {
account.getRoster().markAllAsNotInRoster(); account.getRoster().markAllAsNotInRoster();
} }
this.rosterItems(account, query); this.rosterItems(account, query);
} else if ((packet.hasChild("block", Namespace.BLOCKING) || packet.hasChild("blocklist", Namespace.BLOCKING)) && } else if ((packet.hasChild("block", Namespace.BLOCKING) || packet.hasChild("blocklist", Namespace.BLOCKING)) &&
packet.fromServer(account)) { packet.fromServer(account)) {
// Block list or block push. // Block list or block push.
Log.d(Config.LOGTAG, "Received blocklist update from server"); Log.d(Config.LOGTAG, "Received blocklist update from server");
final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING); final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING);
final Element block = packet.findChild("block", Namespace.BLOCKING); final Element block = packet.findChild("block", Namespace.BLOCKING);
final Collection<Element> items = blocklist != null ? blocklist.getChildren() : final Collection<Element> items = blocklist != null ? blocklist.getChildren() :
(block != null ? block.getChildren() : null); (block != null ? block.getChildren() : null);
// If this is a response to a blocklist query, clear the block list and replace with the new one. // 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. // Otherwise, just update the existing blocklist.
if (packet.getType() == IqPacket.TYPE.RESULT) { if (packet.getType() == IqPacket.TYPE.RESULT) {
account.clearBlocklist(); account.clearBlocklist();
account.getXmppConnection().getFeatures().setBlockListRequested(true); account.getXmppConnection().getFeatures().setBlockListRequested(true);
} }
if (items != null) { if (items != null) {
final Collection<Jid> jids = new ArrayList<>(items.size()); final Collection<Jid> jids = new ArrayList<>(items.size());
// Create a collection of Jids from the packet // Create a collection of Jids from the packet
for (final Element item : items) { for (final Element item : items) {
if (item.getName().equals("item")) { if (item.getName().equals("item")) {
final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
if (jid != null) { if (jid != null) {
jids.add(jid); jids.add(jid);
} }
} }
} }
account.getBlocklist().addAll(jids); account.getBlocklist().addAll(jids);
if (packet.getType() == IqPacket.TYPE.SET) { if (packet.getType() == IqPacket.TYPE.SET) {
boolean removed = false; boolean removed = false;
for(Jid jid : jids) { for (Jid jid : jids) {
removed |= mXmppConnectionService.removeBlockedConversations(account,jid); removed |= mXmppConnectionService.removeBlockedConversations(account, jid);
} }
if (removed) { if (removed) {
mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateConversationUi();
} }
} }
} }
// Update the UI // Update the UI
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
if (packet.getType() == IqPacket.TYPE.SET) { if (packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null); mXmppConnectionService.sendIqPacket(account, response, null);
} }
} else if (packet.hasChild("unblock", Namespace.BLOCKING) && } else if (packet.hasChild("unblock", Namespace.BLOCKING) &&
packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) { packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) {
Log.d(Config.LOGTAG, "Received unblock update from server"); Log.d(Config.LOGTAG, "Received unblock update from server");
final Collection<Element> items = packet.findChild("unblock", Namespace.BLOCKING).getChildren(); final Collection<Element> items = packet.findChild("unblock", Namespace.BLOCKING).getChildren();
if (items.size() == 0) { if (items.size() == 0) {
// No children to unblock == unblock all // No children to unblock == unblock all
account.getBlocklist().clear(); account.getBlocklist().clear();
} else { } else {
final Collection<Jid> jids = new ArrayList<>(items.size()); final Collection<Jid> jids = new ArrayList<>(items.size());
for (final Element item : items) { for (final Element item : items) {
if (item.getName().equals("item")) { if (item.getName().equals("item")) {
final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
if (jid != null) { if (jid != null) {
jids.add(jid); jids.add(jid);
} }
} }
} }
account.getBlocklist().removeAll(jids); account.getBlocklist().removeAll(jids);
} }
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null); mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
|| packet.hasChild("data", "http://jabber.org/protocol/ibb") || packet.hasChild("data", "http://jabber.org/protocol/ibb")
|| packet.hasChild("close","http://jabber.org/protocol/ibb")) { || packet.hasChild("close", "http://jabber.org/protocol/ibb")) {
mXmppConnectionService.getJingleConnectionManager() mXmppConnectionService.getJingleConnectionManager()
.deliverIbbPacket(account, packet); .deliverIbbPacket(account, packet);
} else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet); final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet);
mXmppConnectionService.sendIqPacket(account, response, null); mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("query","jabber:iq:version") && isGet) { } else if (packet.hasChild("query", "jabber:iq:version") && isGet) {
final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet); final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
mXmppConnectionService.sendIqPacket(account,response,null); mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) { } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null); mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("time","urn:xmpp:time") && isGet) { } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) {
final IqPacket response; final IqPacket response;
if (mXmppConnectionService.useTorToConnect() || account.isOnion()) { if (mXmppConnectionService.useTorToConnect() || account.isOnion()) {
response = packet.generateResponse(IqPacket.TYPE.ERROR); response = packet.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error"); final Element error = response.addChild("error");
error.setAttribute("type","cancel"); error.setAttribute("type", "cancel");
error.addChild("not-allowed","urn:ietf:params:xml:ns:xmpp-stanzas"); error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas");
} else { } else {
response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
} }
mXmppConnectionService.sendIqPacket(account,response, null); mXmppConnectionService.sendIqPacket(account, response, null);
} else { } else if (packet.hasChild("pubsub", Namespace.PUBSUB) && packet.getType() == IqPacket.TYPE.SET) {
if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { final Jid server = packet.getFrom();
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
final Element error = response.addChild("error"); final Element publish = pubsub == null ? null : pubsub.findChild("publish");
error.setAttribute("type", "cancel"); final String node = publish == null ? null : publish.getAttribute("node");
error.addChild("feature-not-implemented","urn:ietf:params:xml:ns:xmpp-stanzas"); final Element item = publish == null ? null : publish.findChild("item");
account.getXmppConnection().sendIqPacket(response, null); 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);
}
}
}
} }

View File

@ -22,6 +22,7 @@ import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.ReadByMarker; import eu.siacs.conversations.entities.ReadByMarker;
@ -126,7 +127,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
service.reportBrokenSessionException(e, postpone); service.reportBrokenSessionException(e, postpone);
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
} else { } 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; return null;
} }
} catch (NotEncryptedForThisDeviceException e) { } catch (NotEncryptedForThisDeviceException e) {
@ -264,6 +266,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
packet.getId(), packet.getId(),
Message.STATUS_SEND_FAILED, Message.STATUS_SEND_FAILED,
extractErrorMessage(packet)); 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; return true;
} }
@ -437,6 +450,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
origin = from; origin = from;
} }
//TODO either or is probably fine?
final boolean checkedForDuplicates = serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId); final boolean checkedForDuplicates = serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId);
if (origin != null) { if (origin != null) {
@ -598,7 +612,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} else { } else {
serverMsgIdUpdated = false; 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; return;
} }
} }

View File

@ -341,7 +341,7 @@ public class FileBackend {
} }
} }
public static void close(Closeable stream) { public static void close(final Closeable stream) {
if (stream != null) { if (stream != null) {
try { try {
stream.close(); 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) { if (socket != null) {
try { try {
socket.close(); socket.close();

View File

@ -37,6 +37,7 @@ import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.RawBlockable;
import eu.siacs.conversations.http.services.MuclumbusService; import eu.siacs.conversations.http.services.MuclumbusService;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
@ -272,7 +273,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
} }
public Bitmap get(ListItem item, int size, boolean cachedOnly) { 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); return get((Contact) item, size, cachedOnly);
} else if (item instanceof Bookmark) { } else if (item instanceof Bookmark) {
Bookmark bookmark = (Bookmark) item; Bookmark bookmark = (Bookmark) item;

View File

@ -314,6 +314,12 @@ public class XmppConnectionService extends Service {
} }
account.getRoster().clearPresences(); account.getRoster().clearPresences();
synchronized (account.inProgressConferenceJoins) {
account.inProgressConferenceJoins.clear();
}
synchronized (account.inProgressConferencePings) {
account.inProgressConferencePings.clear();
}
mJingleConnectionManager.cancelInTransmission(); mJingleConnectionManager.cancelInTransmission();
mQuickConversationsService.considerSyncBackground(false); mQuickConversationsService.considerSyncBackground(false);
fetchRosterFromServer(account); fetchRosterFromServer(account);
@ -372,18 +378,37 @@ public class XmppConnectionService extends Service {
} }
List<Conversation> conversations = getConversations(); List<Conversation> conversations = getConversations();
for (Conversation conversation : conversations) { 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); sendUnsentMessages(conversation);
} }
} }
for (Conversation conversation : account.pendingConferenceLeaves) { final List<Conversation> pendingLeaves;
synchronized (account.pendingConferenceLeaves) {
pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves);
account.pendingConferenceLeaves.clear();
}
for (Conversation conversation : pendingLeaves) {
leaveMuc(conversation); leaveMuc(conversation);
} }
account.pendingConferenceLeaves.clear(); final List<Conversation> pendingJoins;
for (Conversation conversation : account.pendingConferenceJoins) { synchronized (account.pendingConferenceJoins) {
pendingJoins = new ArrayList<>(account.pendingConferenceJoins);
account.pendingConferenceJoins.clear();
}
for (Conversation conversation : pendingJoins) {
joinMuc(conversation); joinMuc(conversation);
} }
account.pendingConferenceJoins.clear();
scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
} else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) { } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
resetSendingToWaiting(account); resetSendingToWaiting(account);
@ -586,6 +611,7 @@ public class XmppConnectionService extends Service {
toggleForegroundService(true); toggleForegroundService(true);
} }
String pushedAccountHash = null; String pushedAccountHash = null;
String pushedChannelHash = null;
boolean interactive = false; boolean interactive = false;
if (action != null) { if (action != null) {
final String uuid = intent.getStringExtra("uuid"); final String uuid = intent.getStringExtra("uuid");
@ -698,6 +724,7 @@ public class XmppConnectionService extends Service {
break; break;
case ACTION_FCM_MESSAGE_RECEIVED: case ACTION_FCM_MESSAGE_RECEIVED:
pushedAccountHash = intent.getStringExtra("account"); pushedAccountHash = intent.getStringExtra("account");
pushedChannelHash = intent.getStringExtra("channel");
Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash); Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash);
break; break;
case Intent.ACTION_SEND: case Intent.ACTION_SEND:
@ -711,13 +738,18 @@ public class XmppConnectionService extends Service {
synchronized (this) { synchronized (this) {
WakeLockHelper.acquire(wakeLock); WakeLockHelper.acquire(wakeLock);
boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
HashSet<Account> pingCandidates = new HashSet<>(); final HashSet<Account> pingCandidates = new HashSet<>();
final String androidId = PhoneHelper.getAndroidId(this);
for (Account account : accounts) { for (Account account : accounts) {
final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash);
pingNow |= processAccountState(account, pingNow |= processAccountState(account,
interactive, interactive,
"ui".equals(action), "ui".equals(action),
CryptoHelper.getAccountFingerprint(account, PhoneHelper.getAndroidId(this)).equals(pushedAccountHash), pushWasMeantForThisAccount,
pingCandidates); pingCandidates);
if (pushWasMeantForThisAccount && pushedChannelHash != null) {
checkMucStillJoined(account, pushedAccountHash, androidId);
}
} }
if (pingNow) { if (pingNow) {
for (Account account : pingCandidates) { for (Account account : pingCandidates) {
@ -810,6 +842,20 @@ public class XmppConnectionService extends Service {
return pingNow; 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() { public void reinitializeMuclumbusService() {
mChannelDiscoveryService.initializeMuclumbusService(); mChannelDiscoveryService.initializeMuclumbusService();
} }
@ -848,7 +894,7 @@ public class XmppConnectionService extends Service {
} }
@Override @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()) { switch (message.getEncryption()) {
case Message.ENCRYPTION_NONE: case Message.ENCRYPTION_NONE:
if (message.needsUploading()) { if (message.needsUploading()) {
@ -1995,6 +2046,10 @@ public class XmppConnectionService extends Service {
} }
} }
} }
if (conversation.getMucOptions().push()) {
disableDirectMucPush(conversation);
mPushManagementService.disablePushOnServer(conversation);
}
leaveMuc(conversation); leaveMuc(conversation);
} else { } else {
if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
@ -2432,21 +2487,37 @@ public class XmppConnectionService extends Service {
} }
public void mucSelfPingAndRejoin(final Conversation conversation) { 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 Jid self = conversation.getMucOptions().getSelf().getFullJid();
final IqPacket ping = new IqPacket(IqPacket.TYPE.GET); final IqPacket ping = new IqPacket(IqPacket.TYPE.GET);
ping.setTo(self); ping.setTo(self);
ping.addChild("ping", Namespace.PING); ping.addChild("ping", Namespace.PING);
sendIqPacket(conversation.getAccount(), ping, (account, response) -> { sendIqPacket(conversation.getAccount(), ping, (a, response) -> {
if (response.getType() == IqPacket.TYPE.ERROR) { if (response.getType() == IqPacket.TYPE.ERROR) {
Element error = response.findChild("error"); Element error = response.findChild("error");
if (error == null || error.hasChild("service-unavailable") || error.hasChild("feature-not-implemented") || error.hasChild("item-not-found")) { 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 { } 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); joinMuc(conversation);
} }
} else if (response.getType() == IqPacket.TYPE.RESULT) { } 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) { private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) {
Account account = conversation.getAccount(); final Account account = conversation.getAccount();
account.pendingConferenceJoins.remove(conversation); synchronized (account.pendingConferenceJoins) {
account.pendingConferenceLeaves.remove(conversation); account.pendingConferenceJoins.remove(conversation);
}
synchronized (account.pendingConferenceLeaves) {
account.pendingConferenceLeaves.remove(conversation);
}
if (account.getStatus() == Account.State.ONLINE) { if (account.getStatus() == Account.State.ONLINE) {
synchronized (account.inProgressConferenceJoins) {
account.inProgressConferenceJoins.add(conversation);
}
sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions())); sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
conversation.resetMucOptions(); conversation.resetMucOptions();
if (onConferenceJoined != null) { if (onConferenceJoined != null) {
@ -2523,7 +2601,13 @@ public class XmppConnectionService extends Service {
saveConversationAsBookmark(conversation, null); saveConversationAsBookmark(conversation, null);
} }
} }
sendUnsentMessages(conversation); if (mucOptions.push()) {
enableMucPush(conversation);
}
synchronized (account.inProgressConferenceJoins) {
account.inProgressConferenceJoins.remove(conversation);
sendUnsentMessages(conversation);
}
} }
@Override @Override
@ -2539,9 +2623,13 @@ public class XmppConnectionService extends Service {
public void onFetchFailed(final Conversation conversation, Element error) { public void onFetchFailed(final Conversation conversation, Element error) {
if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": conversation ("+conversation.getJid()+") got archived before IQ result"); Log.d(Config.LOGTAG,account.getJid().asBareJid()+": conversation ("+conversation.getJid()+") got archived before IQ result");
return; return;
} }
if (error != null && "remote-server-not-found".equals(error.getName())) { 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); conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND);
updateConversationUi(); updateConversationUi();
} else { } else {
@ -2552,13 +2640,48 @@ public class XmppConnectionService extends Service {
}); });
updateConversationUi(); updateConversationUi();
} else { } else {
account.pendingConferenceJoins.add(conversation); synchronized (account.pendingConferenceJoins) {
account.pendingConferenceJoins.add(conversation);
}
conversation.resetMucOptions(); conversation.resetMucOptions();
conversation.setHasMessagesLeftOnServer(false); conversation.setHasMessagesLeftOnServer(false);
updateConversationUi(); 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) { private void fetchConferenceMembers(final Conversation conversation) {
final Account account = conversation.getAccount(); final Account account = conversation.getAccount();
final AxolotlService axolotlService = account.getAxolotlService(); final AxolotlService axolotlService = account.getAxolotlService();
@ -2734,9 +2857,13 @@ public class XmppConnectionService extends Service {
} }
private void leaveMuc(Conversation conversation, boolean now) { private void leaveMuc(Conversation conversation, boolean now) {
Account account = conversation.getAccount(); final Account account = conversation.getAccount();
account.pendingConferenceJoins.remove(conversation); synchronized (account.pendingConferenceJoins) {
account.pendingConferenceLeaves.remove(conversation); account.pendingConferenceJoins.remove(conversation);
}
synchronized (account.pendingConferenceLeaves) {
account.pendingConferenceLeaves.remove(conversation);
}
if (account.getStatus() == Account.State.ONLINE || now) { if (account.getStatus() == Account.State.ONLINE || now) {
sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions())); sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions()));
conversation.getMucOptions().setOffline(); conversation.getMucOptions().setOffline();
@ -2746,7 +2873,9 @@ public class XmppConnectionService extends Service {
} }
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid()); Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid());
} else { } 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()) { for (Account account : getAccounts()) {
if (account.isOnlineAndConnected() && mPushManagementService.available(account)) { if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
mPushManagementService.registerPushTokenOnServer(account); mPushManagementService.registerPushTokenOnServer(account);
//TODO renew mucs
} }
} }
} }
@ -4121,17 +4251,15 @@ public class XmppConnectionService extends Service {
public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) { public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) {
if (blockable != null && blockable.getBlockedJid() != null) { if (blockable != null && blockable.getBlockedJid() != null) {
final Jid jid = blockable.getBlockedJid(); final Jid jid = blockable.getBlockedJid();
this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), new OnIqPacketReceived() { this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), (a, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
@Override a.getBlocklist().add(jid);
public void onIqPacketReceived(final Account account, final IqPacket packet) { updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
if (packet.getType() == IqPacket.TYPE.RESULT) { }
account.getBlocklist().add(jid); });
updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); if (blockable.getBlockedJid().isFullJid()) {
} return false;
} } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
});
if (removeBlockedConversations(blockable.getAccount(), jid)) {
updateConversationUi(); updateConversationUi();
return true; return true;
} else { } else {

View File

@ -28,14 +28,18 @@ public final class BlockContactDialog {
final String value; final String value;
@StringRes int res; @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); builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain);
value = Jid.ofDomain(blockable.getJid().getDomain()).toString(); value = Jid.ofDomain(blockable.getJid().getDomain()).toString();
res = isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text; res = isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text;
} else { } else {
int resBlockAction = blockable instanceof Conversation && ((Conversation) blockable).isWithStranger() ? R.string.block_stranger : R.string.action_block_contact; 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); 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; res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text;
} }
binding.text.setText(JidDialog.style(xmppActivity, res, value)); binding.text.setText(JidDialog.style(xmppActivity, res, value));

View File

@ -10,7 +10,10 @@ import java.util.Collections;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Contact; 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.ui.interfaces.OnBackendConnected;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import rocks.xmpp.addr.Jid; import rocks.xmpp.addr.Jid;
@ -23,7 +26,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getListView().setOnItemLongClickListener((parent, view, position, id) -> { getListView().setOnItemLongClickListener((parent, view, position, id) -> {
BlockContactDialog.show(BlocklistActivity.this, (Contact) getListItems().get(position)); BlockContactDialog.show(BlocklistActivity.this, (Blockable) getListItems().get(position));
return true; return true;
}); });
this.binding.fab.show(); this.binding.fab.show();
@ -50,9 +53,14 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
getListItems().clear(); getListItems().clear();
if (account != null) { if (account != null) {
for (final Jid jid : account.getBlocklist()) { for (final Jid jid : account.getBlocklist()) {
final Contact contact = account.getRoster().getContact(jid); ListItem item;
if (contact.match(this, needle) && contact.isBlocked()) { if (jid.isFullJid()) {
getListItems().add(contact); item = new RawBlockable(account, jid);
} else {
item = account.getRoster().getContact(jid);
}
if (item.match(this, needle)) {
getListItems().add(item);
} }
} }
Collections.sort(getListItems()); Collections.sort(getListItems());
@ -78,8 +86,8 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
); );
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> { dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
Contact contact = account.getRoster().getContact(contactJid); Blockable blockable = new RawBlockable(account, contactJid);
if (xmppConnectionService.sendBlockRequest(contact, false)) { if (xmppConnectionService.sendBlockRequest(blockable, false)) {
Toast.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show(); Toast.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show();
} }
return true; return true;
@ -101,4 +109,5 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) { public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) {
refreshUi(); refreshUi();
} }
} }

View File

@ -83,7 +83,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
} }
@Override @Override
public void userInputRequried(PendingIntent pi, Conversation object) { public void userInputRequired(PendingIntent pi, Conversation object) {
} }
}; };

View File

@ -632,7 +632,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
@Override @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 @Override
public void userInputRequried(PendingIntent pi, Message message) { public void userInputRequired(PendingIntent pi, Message message) {
hidePrepareFileToast(prepareFileToast); hidePrepareFileToast(prepareFileToast);
} }
}); });
@ -688,7 +688,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
new UiCallback<Message>() { new UiCallback<Message>() {
@Override @Override
public void userInputRequried(PendingIntent pi, Message object) { public void userInputRequired(PendingIntent pi, Message object) {
hidePrepareFileToast(prepareFileToast); hidePrepareFileToast(prepareFileToast);
} }
@ -1326,7 +1326,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
new UiCallback<Contact>() { new UiCallback<Contact>() {
@Override @Override
public void userInputRequried(PendingIntent pi, Contact contact) { public void userInputRequired(PendingIntent pi, Contact contact) {
startPendingIntent(pi, attachmentChoice); startPendingIntent(pi, attachmentChoice);
} }
@ -2284,7 +2284,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
status = Presence.Status.OFFLINE; status = Presence.Status.OFFLINE;
} }
this.binding.textSendButton.setTag(action); 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() { protected void updateStatusMessages() {
@ -2456,7 +2459,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
new UiCallback<Contact>() { new UiCallback<Contact>() {
@Override @Override
public void userInputRequried(PendingIntent pi, Contact contact) { public void userInputRequired(PendingIntent pi, Contact contact) {
startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE); startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE);
} }
@ -2512,7 +2515,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
new UiCallback<Message>() { new UiCallback<Message>() {
@Override @Override
public void userInputRequried(PendingIntent pi, Message message) { public void userInputRequired(PendingIntent pi, Message message) {
startPendingIntent(pi, REQUEST_SEND_MESSAGE); startPendingIntent(pi, REQUEST_SEND_MESSAGE);
} }

View File

@ -104,7 +104,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() { private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
@Override @Override
public void userInputRequried(final PendingIntent pi, final Avatar avatar) { public void userInputRequired(final PendingIntent pi, final Avatar avatar) {
finishInitialSetup(avatar); finishInitialSetup(avatar);
} }
@ -917,7 +917,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} }
@Override @Override
public void userInputRequried(PendingIntent pi, String object) { public void userInputRequired(PendingIntent pi, String object) {
mPendingPresenceTemplate.push(template); mPendingPresenceTemplate.push(template);
try { try {
startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0); startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0);

View File

@ -23,8 +23,10 @@ import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController; import org.osmdroid.api.IMapController;
import org.osmdroid.config.Configuration; import org.osmdroid.config.Configuration;
import org.osmdroid.config.IConfigurationProvider; import org.osmdroid.config.IConfigurationProvider;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.tileprovider.tilesource.XYTileSource; import org.osmdroid.tileprovider.tilesource.XYTileSource;
import org.osmdroid.util.GeoPoint; import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.CustomZoomButtonsController;
import org.osmdroid.views.MapView; import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.Overlay; import org.osmdroid.views.overlay.Overlay;
@ -72,15 +74,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
protected void updateLocationMarkers() { protected void updateLocationMarkers() {
clearMarkers(); 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 @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -103,7 +97,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
final IConfigurationProvider config = Configuration.getInstance(); final IConfigurationProvider config = Configuration.getInstance();
config.load(ctx, getPreferences()); 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)) { if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
try { try {
config.setHttpProxy(HttpConnectionManager.getProxy()); config.setHttpProxy(HttpConnectionManager.getProxy());
@ -111,17 +105,6 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
throw new RuntimeException("Unable to configure proxy"); 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 @Override
@ -150,8 +133,8 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
protected void setupMapView(MapView mapView, final GeoPoint pos) { protected void setupMapView(MapView mapView, final GeoPoint pos) {
map = mapView; map = mapView;
map.setTileSource(tileSource()); map.setTileSource(TileSourceFactory.MAPNIK);
map.setBuiltInZoomControls(false); map.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
map.setMultiTouchControls(true); map.setMultiTouchControls(true);
map.setTilesScaledToDpi(true); map.setTilesScaledToDpi(true);
mapController = map.getController(); mapController = map.getController();
@ -251,7 +234,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
requestLocationUpdates(); requestLocationUpdates();
updateLocationMarkers(); updateLocationMarkers();
updateUi(); updateUi();
map.setTileSource(tileSource()); map.setTileSource(TileSourceFactory.MAPNIK);
map.setTilesScaledToDpi(true); map.setTilesScaledToDpi(true);
if (mapAtInitialLoc()) { if (mapAtInitialLoc()) {

View File

@ -174,7 +174,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
} }
@Override @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 @Override
public void userInputRequried(PendingIntent pi, Conversation object) { public void userInputRequired(PendingIntent pi, Conversation object) {
} }
}); });

View File

@ -7,5 +7,5 @@ public interface UiCallback<T> {
void error(int errorCode, T object); void error(int errorCode, T object);
void userInputRequried(PendingIntent pi, T object); void userInputRequired(PendingIntent pi, T object);
} }

View File

@ -137,7 +137,7 @@ public abstract class XmppActivity extends ActionBarActivity {
} }
@Override @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<String>() { xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback<String>() {
@Override @Override
public void userInputRequried(PendingIntent pi, String signature) { public void userInputRequired(PendingIntent pi, String signature) {
try { try {
startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
} catch (final SendIntentException ignored) { } catch (final SendIntentException ignored) {
@ -625,7 +625,7 @@ public abstract class XmppActivity extends ActionBarActivity {
} }
@Override @Override
public void userInputRequried(PendingIntent pi, Account object) { public void userInputRequired(PendingIntent pi, Account object) {
try { try {
startIntentSenderForResult(pi.getIntentSender(), startIntentSenderForResult(pi.getIntentSender(),
REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0); REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);

View File

@ -246,8 +246,12 @@ public final class CryptoHelper {
return prettifyFingerprintCert(bytesToHex(fingerprint)); 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) { 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) { public static String getFingerprint(String value) {

View File

@ -7,7 +7,7 @@ import org.hsluv.HUSLColorConverter;
import java.security.MessageDigest; import java.security.MessageDigest;
public class XEP0392Helper { class XEP0392Helper {
private static double angle(String nickname) { private static double angle(String nickname) {
try { try {
@ -20,7 +20,7 @@ public class XEP0392Helper {
} }
} }
public static int rgbFromNick(String name) { static int rgbFromNick(String name) {
double[] hsluv = new double[3]; double[] hsluv = new double[3];
hsluv[0] = angle(name) * 360; hsluv[0] = angle(name) * 360;
hsluv[1] = 100; hsluv[1] = 100;

View File

@ -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_S5B = "urn:xmpp:jingle:transports:s5b:1";
public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb: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 PING = "urn:xmpp:ping";
public static final String PUSH = "urn:xmpp:push:0";
public static final String COMMANDS = "http://jabber.org/protocol/commands";
} }

View File

@ -6,14 +6,15 @@ import android.util.Xml;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
public class XmlReader { public class XmlReader implements Closeable {
private XmlPullParser parser; private final XmlPullParser parser;
private InputStream is; private InputStream is;
public XmlReader() { public XmlReader() {
@ -48,6 +49,11 @@ public class XmlReader {
} }
} }
@Override
public void close() {
this.is = null;
}
public Tag readTag() throws IOException { public Tag readTag() throws IOException {
try { try {
while (this.is != null && parser.next() != XmlPullParser.END_DOCUMENT) { while (this.is != null && parser.next() != XmlPullParser.END_DOCUMENT) {

View File

@ -1439,15 +1439,8 @@ public class XmppConnection implements Runnable {
} }
private void forceCloseSocket() { private void forceCloseSocket() {
if (socket != null) { FileBackend.close(this.socket);
try { FileBackend.close(this.tagReader);
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");
}
} }
public void interrupt() { public void interrupt() {
@ -1458,7 +1451,7 @@ public class XmppConnection implements Runnable {
public void disconnect(final boolean force) { public void disconnect(final boolean force) {
interrupt(); 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) { if (force) {
forceCloseSocket(); forceCloseSocket();
} else { } else {
@ -1798,8 +1791,8 @@ public class XmppConnection implements Runnable {
} }
public boolean push() { public boolean push() {
return hasDiscoFeature(account.getJid().asBareJid(), "urn:xmpp:push:0") return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
|| hasDiscoFeature(Jid.of(account.getServer()), "urn:xmpp:push:0"); || hasDiscoFeature(Jid.of(account.getServer()), Namespace.PUSH);
} }
public boolean rosterVersioning() { public boolean rosterVersioning() {

View File

@ -776,4 +776,10 @@
<string name="open_with">Отваряне с…</string> <string name="open_with">Отваряне с…</string>
<string name="set_profile_picture">Профилна снимка за Conversations</string> <string name="set_profile_picture">Профилна снимка за Conversations</string>
<string name="choose_account">Изберете профил</string> <string name="choose_account">Изберете профил</string>
<string name="restore_backup">Възстановяване от резервно копие</string>
<string name="restore">Възстановяване</string>
<string name="enter_password_to_restore">Въведете паролата си за профила %s, за да направите възстановяване от резервно копие.</string>
<string name="restore_warning">Не използвайте възможността за възстановяване от резервно копие, за да клонирате (да изпълнявате едновременно) инсталацията. Възстановяването от резервно копие е предназначено за мигриране или в случай, че сте загубили устройството си.</string>
<string name="unable_to_restore_backup">Не може да се направи възстановяване от резервно копие.</string>
<string name="unable_to_decrypt_backup">Резервното копие не може да бъде дешифрирано. Правилна ли е паролата?</string>
</resources> </resources>

View File

@ -17,6 +17,8 @@
<string name="action_unblock_contact">Kontakt entsperren</string> <string name="action_unblock_contact">Kontakt entsperren</string>
<string name="action_block_domain">Domain sperren</string> <string name="action_block_domain">Domain sperren</string>
<string name="action_unblock_domain">Domain entsperren</string> <string name="action_unblock_domain">Domain entsperren</string>
<string name="action_block_participant">Teilnehmer sperren</string>
<string name="action_unblock_participant">Teilnehmer entsperren</string>
<string name="title_activity_manage_accounts">Konten verwalten</string> <string name="title_activity_manage_accounts">Konten verwalten</string>
<string name="title_activity_settings">Einstellungen</string> <string name="title_activity_settings">Einstellungen</string>
<string name="title_activity_sharewith">Mit Unterhaltung teilen</string> <string name="title_activity_sharewith">Mit Unterhaltung teilen</string>

View File

@ -17,6 +17,8 @@
<string name="action_unblock_contact">Desbloquear contacto</string> <string name="action_unblock_contact">Desbloquear contacto</string>
<string name="action_block_domain">Bloquear dominio</string> <string name="action_block_domain">Bloquear dominio</string>
<string name="action_unblock_domain">Desbloquear dominio</string> <string name="action_unblock_domain">Desbloquear dominio</string>
<string name="action_block_participant">Bloquear persoa</string>
<string name="action_unblock_participant">Desbloquear persoa</string>
<string name="title_activity_manage_accounts">Xestionar contas</string> <string name="title_activity_manage_accounts">Xestionar contas</string>
<string name="title_activity_settings">Axustes</string> <string name="title_activity_settings">Axustes</string>
<string name="title_activity_sharewith">Compartir na conversa</string> <string name="title_activity_sharewith">Compartir na conversa</string>

View File

@ -17,6 +17,8 @@
<string name="action_unblock_contact">Deblochează contact</string> <string name="action_unblock_contact">Deblochează contact</string>
<string name="action_block_domain">Blochează domeniu</string> <string name="action_block_domain">Blochează domeniu</string>
<string name="action_unblock_domain">Deblochează domeniu</string> <string name="action_unblock_domain">Deblochează domeniu</string>
<string name="action_block_participant">Blochează participant</string>
<string name="action_unblock_participant">Deblochează participant</string>
<string name="title_activity_manage_accounts">Configurează conturile</string> <string name="title_activity_manage_accounts">Configurează conturile</string>
<string name="title_activity_settings">Setări</string> <string name="title_activity_settings">Setări</string>
<string name="title_activity_sharewith">Partajează într-o conversație</string> <string name="title_activity_sharewith">Partajează într-o conversație</string>

View File

@ -3,8 +3,10 @@
<string name="action_settings">Настройки</string> <string name="action_settings">Настройки</string>
<string name="action_add">Новая беседа</string> <string name="action_add">Новая беседа</string>
<string name="action_accounts">Управление аккаунтами</string> <string name="action_accounts">Управление аккаунтами</string>
<string name="action_end_conversation">Закрыть текущую беседу</string>
<string name="action_contact_details">Сведения о контакте</string> <string name="action_contact_details">Сведения о контакте</string>
<string name="action_muc_details">Подробности конференции</string> <string name="action_muc_details">Подробности конференции</string>
<string name="channel_details">Сведения о канале</string>
<string name="action_secure">Защищённая беседа</string> <string name="action_secure">Защищённая беседа</string>
<string name="action_add_account">Добавить аккаунт</string> <string name="action_add_account">Добавить аккаунт</string>
<string name="action_edit_contact">Редактировать контакт</string> <string name="action_edit_contact">Редактировать контакт</string>
@ -48,6 +50,7 @@
<string name="share_with">Поделиться с</string> <string name="share_with">Поделиться с</string>
<string name="start_conversation">Начать беседу</string> <string name="start_conversation">Начать беседу</string>
<string name="invite_contact">Пригласить собеседника</string> <string name="invite_contact">Пригласить собеседника</string>
<string name="invite">Пригласить</string>
<string name="contacts">Контакты</string> <string name="contacts">Контакты</string>
<string name="contact">Контакт</string> <string name="contact">Контакт</string>
<string name="cancel">Отмена</string> <string name="cancel">Отмена</string>
@ -78,6 +81,7 @@
<string name="clear_histor_msg">Вы хотите удалить все сообщения в этой беседе?\n\n<b>Предупреждение:</b> Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах.</string> <string name="clear_histor_msg">Вы хотите удалить все сообщения в этой беседе?\n\n<b>Предупреждение:</b> Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах.</string>
<string name="delete_file_dialog">Удалить файл</string> <string name="delete_file_dialog">Удалить файл</string>
<string name="delete_file_dialog_msg">Вы уверены, что хотите удалить этот файл?\n\n<b>Предупреждение:</b> Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах.</string> <string name="delete_file_dialog_msg">Вы уверены, что хотите удалить этот файл?\n\n<b>Предупреждение:</b> Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах.</string>
<string name="also_end_conversation">Закрыть эту беседу</string>
<string name="choose_presence">Выберите устройство</string> <string name="choose_presence">Выберите устройство</string>
<string name="send_unencrypted_message">Нешифрованное сообщение</string> <string name="send_unencrypted_message">Нешифрованное сообщение</string>
<string name="send_message">Сообщение</string> <string name="send_message">Сообщение</string>
@ -158,15 +162,18 @@
<string name="mgmt_account_disable">Временно отключить</string> <string name="mgmt_account_disable">Временно отключить</string>
<string name="mgmt_account_publish_avatar">Разместить аватар</string> <string name="mgmt_account_publish_avatar">Разместить аватар</string>
<string name="mgmt_account_publish_pgp">Анонсировать OpenPGP ключ</string> <string name="mgmt_account_publish_pgp">Анонсировать OpenPGP ключ</string>
<string name="unpublish_pgp">Удалить открытый OpenPGP ключ</string> <string name="unpublish_pgp">Удалить открытый ключ OpenPGP</string>
<string name="unpublish_pgp_message">Вы действительно хотите удалить ваш OpenPGP публичный ключ из опубликованных?\nВаши собеседники не смогут больше отправлять вам зашифрованные OpenPGP сообщения.</string> <string name="unpublish_pgp_message">Вы действительно хотите удалить ваш OpenPGP публичный ключ из опубликованных?\nВаши собеседники не смогут больше отправлять вам зашифрованные OpenPGP сообщения.</string>
<string name="openpgp_has_been_published">Открытый ключ OpenPGP был опубликован.</string> <string name="openpgp_has_been_published">Открытый ключ OpenPGP был опубликован.</string>
<string name="mgmt_account_enable">Включить аккаунт</string> <string name="mgmt_account_enable">Включить аккаунт</string>
<string name="mgmt_account_are_you_sure">Вы уверены?</string> <string name="mgmt_account_are_you_sure">Вы уверены?</string>
<string name="mgmt_account_delete_confirm_text">Если вы удалите аккаунт, будет потеряна вся история переписки.</string> <string name="mgmt_account_delete_confirm_text">Если вы удалите аккаунт, будет потеряна вся история переписки.</string>
<string name="attach_record_voice">Запись голоса</string> <string name="attach_record_voice">Запись голоса</string>
<string name="account_settings_jabber_id">XMPP-адрес</string>
<string name="block_jabber_id">Заблокировать XMPP-адрес</string>
<string name="account_settings_example_jabber_id">username@example.com</string> <string name="account_settings_example_jabber_id">username@example.com</string>
<string name="password">Пароль</string> <string name="password">Пароль</string>
<string name="invalid_jid">Недопустимый XMPP-адрес</string>
<string name="error_out_of_memory">Недостаточно памяти. Изображение слишком большое</string> <string name="error_out_of_memory">Недостаточно памяти. Изображение слишком большое</string>
<string name="add_phone_book_text">Вы хотите добавить %s в вашу адресную книгу?</string> <string name="add_phone_book_text">Вы хотите добавить %s в вашу адресную книгу?</string>
<string name="server_info_show_more">Информация о сервере</string> <string name="server_info_show_more">Информация о сервере</string>
@ -201,6 +208,7 @@
<string name="fetching_keys">Получение ключей…</string> <string name="fetching_keys">Получение ключей…</string>
<string name="done">Готово</string> <string name="done">Готово</string>
<string name="decrypt">Расшифровать</string> <string name="decrypt">Расшифровать</string>
<string name="bookmarks">Закладки</string>
<string name="search">Поиск</string> <string name="search">Поиск</string>
<string name="enter_contact">Добавить контакт</string> <string name="enter_contact">Добавить контакт</string>
<string name="delete_contact">Удалить контакт</string> <string name="delete_contact">Удалить контакт</string>
@ -213,6 +221,8 @@
<string name="join">Присоединиться</string> <string name="join">Присоединиться</string>
<string name="save_as_bookmark">Сохранить закладку</string> <string name="save_as_bookmark">Сохранить закладку</string>
<string name="delete_bookmark">Удалить закладку</string> <string name="delete_bookmark">Удалить закладку</string>
<string name="destroy_room">Уничтожить конференцию</string>
<string name="destroy_channel">Уничтожить канал</string>
<string name="bookmark_already_exists">Такая закладка уже существует</string> <string name="bookmark_already_exists">Такая закладка уже существует</string>
<string name="action_edit_subject">Редактировать тему конференции</string> <string name="action_edit_subject">Редактировать тему конференции</string>
<string name="topic">Тема</string> <string name="topic">Тема</string>
@ -253,6 +263,7 @@
<string name="pref_allow_message_correction_summary">Позволить контактам редактировать сообщения</string> <string name="pref_allow_message_correction_summary">Позволить контактам редактировать сообщения</string>
<string name="pref_expert_options">Расширенные настройки</string> <string name="pref_expert_options">Расширенные настройки</string>
<string name="pref_expert_options_summary">Пожалуйста, будьте осторожны с данными настройками</string> <string name="pref_expert_options_summary">Пожалуйста, будьте осторожны с данными настройками</string>
<string name="title_activity_about_x">О %s</string>
<string name="title_pref_quiet_hours">Тихие часы</string> <string name="title_pref_quiet_hours">Тихие часы</string>
<string name="title_pref_quiet_hours_start_time">Начало</string> <string name="title_pref_quiet_hours_start_time">Начало</string>
<string name="title_pref_quiet_hours_end_time">Окончание</string> <string name="title_pref_quiet_hours_end_time">Окончание</string>
@ -281,6 +292,9 @@
<string name="send_again">Отправить ещё раз</string> <string name="send_again">Отправить ещё раз</string>
<string name="file_url">URL файла</string> <string name="file_url">URL файла</string>
<string name="url_copied_to_clipboard">Ссылка скопирована в буфер обмена</string> <string name="url_copied_to_clipboard">Ссылка скопирована в буфер обмена</string>
<string name="jabber_id_copied_to_clipboard">XMPP-адрес скопирован в буфер обмена</string>
<string name="error_message_copied_to_clipboard">Сообщение об ошибке скопировано в буфер обмена</string>
<string name="web_address">веб-адрес</string>
<string name="scan_qr_code">Сканировать 2D штрихкод</string> <string name="scan_qr_code">Сканировать 2D штрихкод</string>
<string name="show_qr_code">Показать 2D штрихкод</string> <string name="show_qr_code">Показать 2D штрихкод</string>
<string name="show_block_list">Показать чёрный список</string> <string name="show_block_list">Показать чёрный список</string>
@ -289,6 +303,14 @@
<string name="try_again">Повторить</string> <string name="try_again">Повторить</string>
<string name="pref_keep_foreground_service">Оставить службу на переднем плане</string> <string name="pref_keep_foreground_service">Оставить службу на переднем плане</string>
<string name="pref_keep_foreground_service_summary">Не позволяет операционной системе закрыть ваше соединение</string> <string name="pref_keep_foreground_service_summary">Не позволяет операционной системе закрыть ваше соединение</string>
<string name="pref_create_backup">Создать резервную копию</string>
<string name="pref_create_backup_summary">Файлы резервной копии будут сохранены в %s</string>
<string name="notification_create_backup_title">Создание резервной копии</string>
<string name="notification_backup_created_title">Ваша резервная копия была создана</string>
<string name="notification_backup_created_subtitle">Файлы резервной копии сохранены в %s</string>
<string name="restoring_backup">Восстановление из резервной копии</string>
<string name="notification_restored_backup_title">Восстановление из резервной копии выполнено</string>
<string name="notification_restored_backup_subtitle">Не забудьте включить аккаунт</string>
<string name="choose_file">Выбрать файл</string> <string name="choose_file">Выбрать файл</string>
<string name="receiving_x_file">%1$s загружается (%2$d%% выполнено)</string> <string name="receiving_x_file">%1$s загружается (%2$d%% выполнено)</string>
<string name="download_x_file">Загрузить %s</string> <string name="download_x_file">Загрузить %s</string>
@ -311,7 +333,7 @@
<string name="conference_creation_failed">Не удалось создать конференцию!</string> <string name="conference_creation_failed">Не удалось создать конференцию!</string>
<string name="account_image_description">Аватар аккаунта</string> <string name="account_image_description">Аватар аккаунта</string>
<string name="copy_omemo_clipboard_description">Скопировать OMEMO-отпечаток в буфер обмена</string> <string name="copy_omemo_clipboard_description">Скопировать OMEMO-отпечаток в буфер обмена</string>
<string name="regenerate_omemo_key">Создать заново ключ OMEMO</string> <string name="regenerate_omemo_key">Создать ключ OMEMO заново</string>
<string name="clear_other_devices">Очистить устройства</string> <string name="clear_other_devices">Очистить устройства</string>
<string name="clear_other_devices_desc">Вы уверены, что хотите очистить все остальные устройства из анонса ключей OMEMO? При соединении устройств в следующий раз новые ключи анонсируются автоматически, но устройства могут не получить сообщения, посланные до этого.</string> <string name="clear_other_devices_desc">Вы уверены, что хотите очистить все остальные устройства из анонса ключей OMEMO? При соединении устройств в следующий раз новые ключи анонсируются автоматически, но устройства могут не получить сообщения, посланные до этого.</string>
<string name="error_no_keys_to_trust_server_error">Для этого контакта не существует доступных ключей.\nПопытка получения новых ключей с сервера оказалась неудачной. Возможно, что-то не так с сервером вашего собеседника.</string> <string name="error_no_keys_to_trust_server_error">Для этого контакта не существует доступных ключей.\nПопытка получения новых ключей с сервера оказалась неудачной. Возможно, что-то не так с сервером вашего собеседника.</string>
@ -366,8 +388,8 @@
<string name="hide_offline">Скрыть пользователей вне сети</string> <string name="hide_offline">Скрыть пользователей вне сети</string>
<string name="contact_is_typing">%s печатает…</string> <string name="contact_is_typing">%s печатает…</string>
<string name="contact_has_stopped_typing">%s прекратил набор</string> <string name="contact_has_stopped_typing">%s прекратил набор</string>
<string name="contacts_are_typing">%s набирает...</string> <string name="contacts_are_typing">%s печатают...</string>
<string name="contacts_have_stopped_typing">%s перестал печатать</string> <string name="contacts_have_stopped_typing">%s перестали печатать</string>
<string name="pref_chat_states">Оповещения о наборе</string> <string name="pref_chat_states">Оповещения о наборе</string>
<string name="pref_chat_states_summary">Позволяет вашим контактам видеть когда вы пишете им новое сообщение</string> <string name="pref_chat_states_summary">Позволяет вашим контактам видеть когда вы пишете им новое сообщение</string>
<string name="send_location">Отправить местоположение</string> <string name="send_location">Отправить местоположение</string>
@ -395,7 +417,8 @@
<string name="recently_used">Последнее выбранное</string> <string name="recently_used">Последнее выбранное</string>
<string name="choose_quick_action">Выбрать быстрое действие</string> <string name="choose_quick_action">Выбрать быстрое действие</string>
<string name="search_contacts">Поиск контактов</string> <string name="search_contacts">Поиск контактов</string>
<string name="send_private_message">Отправить частное сообщение</string> <string name="search_bookmarks">Поиск закладок</string>
<string name="send_private_message">Отправить личное сообщение</string>
<string name="user_has_left_conference">%1$s покинул конференцию!</string> <string name="user_has_left_conference">%1$s покинул конференцию!</string>
<string name="username">Имя пользователя</string> <string name="username">Имя пользователя</string>
<string name="username_hint">Имя пользователя</string> <string name="username_hint">Имя пользователя</string>
@ -409,8 +432,8 @@
<string name="account_status_host_unknown">Сервер не ответственен за домен</string> <string name="account_status_host_unknown">Сервер не ответственен за домен</string>
<string name="server_info_broken">Повреждено</string> <string name="server_info_broken">Повреждено</string>
<string name="pref_presence_settings">Доступность</string> <string name="pref_presence_settings">Доступность</string>
<string name="pref_away_when_screen_off">Вышел когда экран выключен</string> <string name="pref_away_when_screen_off">\"Отошёл\" когда экран выключен</string>
<string name="pref_away_when_screen_off_summary">Отмечает ваш ресурс как «вышел» когда экран выключен</string> <string name="pref_away_when_screen_off_summary">Отмечает ваш ресурс как «отошёл» когда экран выключен</string>
<string name="pref_dnd_on_silent_mode">\"Не беспокоить\" в беззвучном режиме</string> <string name="pref_dnd_on_silent_mode">\"Не беспокоить\" в беззвучном режиме</string>
<string name="pref_dnd_on_silent_mode_summary">Помечать ресурс как \"Не беспокоить\", когда устройство в беззвучном режиме</string> <string name="pref_dnd_on_silent_mode_summary">Помечать ресурс как \"Не беспокоить\", когда устройство в беззвучном режиме</string>
<string name="pref_treat_vibrate_as_silent">Не доступен в режиме вибрации</string> <string name="pref_treat_vibrate_as_silent">Не доступен в режиме вибрации</string>
@ -430,7 +453,7 @@
<string name="certificate_chain_is_not_trusted">Цепочка сертификата не доверена</string> <string name="certificate_chain_is_not_trusted">Цепочка сертификата не доверена</string>
<string name="action_renew_certificate">Обновить сертификат</string> <string name="action_renew_certificate">Обновить сертификат</string>
<string name="error_fetching_omemo_key">Ошибка при получении OMEMO ключа!</string> <string name="error_fetching_omemo_key">Ошибка при получении OMEMO ключа!</string>
<string name="verified_omemo_key_with_certificate">Проверен OMEMO ключ с сертификатом!</string> <string name="verified_omemo_key_with_certificate">Ключ OMEMO проверен с сертификатом!</string>
<string name="device_does_not_support_certificates">Ваше устройство не поддерживает выбор клиентских сертификатов!</string> <string name="device_does_not_support_certificates">Ваше устройство не поддерживает выбор клиентских сертификатов!</string>
<string name="pref_connection_options">Подключение</string> <string name="pref_connection_options">Подключение</string>
<string name="pref_use_tor">Соединение через Tor</string> <string name="pref_use_tor">Соединение через Tor</string>
@ -459,6 +482,8 @@
<string name="notify_only_when_highlighted">Уведомлять только при упоминании</string> <string name="notify_only_when_highlighted">Уведомлять только при упоминании</string>
<string name="notify_never">Без уведомления</string> <string name="notify_never">Без уведомления</string>
<string name="notify_paused">Уведомления приостановлены</string> <string name="notify_paused">Уведомления приостановлены</string>
<string name="pref_picture_compression">Сжатие изображений</string>
<string name="pref_picture_compression_summary">Изменять размер и сжимать изображения</string>
<string name="always">Всегда</string> <string name="always">Всегда</string>
<string name="automatically">Автоматически</string> <string name="automatically">Автоматически</string>
<string name="battery_optimizations_enabled">Оптимизации энергопотребления разрешены</string> <string name="battery_optimizations_enabled">Оптимизации энергопотребления разрешены</string>
@ -475,9 +500,12 @@
<string name="security_error_invalid_file_access">Ошибка безопасности: недействительный доступ к файлу</string> <string name="security_error_invalid_file_access">Ошибка безопасности: недействительный доступ к файлу</string>
<string name="no_application_to_share_uri">Не найдено приложения для отправки</string> <string name="no_application_to_share_uri">Не найдено приложения для отправки</string>
<string name="share_uri_with">Отправить URI…</string> <string name="share_uri_with">Отправить URI…</string>
<string name="your_full_jid_will_be">Ваш полный XMPP-адрес будет: %s</string>
<string name="create_account">Создать аккаунт</string> <string name="create_account">Создать аккаунт</string>
<string name="use_own_provider">Использовать свой провайдер</string> <string name="use_own_provider">Использовать свой провайдер</string>
<string name="pick_your_username">Выберите имя пользователя</string> <string name="pick_your_username">Выберите имя пользователя</string>
<string name="pref_manually_change_presence">Управлять доступностью вручную</string>
<string name="pref_manually_change_presence_summary">Устанавливать свою доступность при редактировании статусного сообщения</string>
<string name="status_message">Статусное собщение</string> <string name="status_message">Статусное собщение</string>
<string name="presence_chat">Свободен для общения</string> <string name="presence_chat">Свободен для общения</string>
<string name="presence_online">В сети</string> <string name="presence_online">В сети</string>
@ -491,6 +519,7 @@
<string name="choose_participants">Выбрать участников</string> <string name="choose_participants">Выбрать участников</string>
<string name="creating_conference">Создание конференции…</string> <string name="creating_conference">Создание конференции…</string>
<string name="invite_again">Пригласить ещё раз</string> <string name="invite_again">Пригласить ещё раз</string>
<string name="gp_disable">Выключен</string>
<string name="gp_short">Короткий</string> <string name="gp_short">Короткий</string>
<string name="gp_medium">Средний</string> <string name="gp_medium">Средний</string>
<string name="gp_long">Длинный</string> <string name="gp_long">Длинный</string>
@ -506,7 +535,7 @@
<string name="type_pc">Компьютер</string> <string name="type_pc">Компьютер</string>
<string name="type_phone">Телефон</string> <string name="type_phone">Телефон</string>
<string name="type_tablet">Планшет</string> <string name="type_tablet">Планшет</string>
<string name="type_web">Веб браузер</string> <string name="type_web">Веб-браузер</string>
<string name="type_console">Консоль</string> <string name="type_console">Консоль</string>
<string name="payment_required">Требуется оплата</string> <string name="payment_required">Требуется оплата</string>
<string name="missing_internet_permission">Доступ в интернет запрещён</string> <string name="missing_internet_permission">Доступ в интернет запрещён</string>
@ -548,10 +577,10 @@
<string name="pref_clean_private_storage_summary">Очистить закрытое хранилище, где хранятся файлы (Файлы можно заново скачать с сервера)</string> <string name="pref_clean_private_storage_summary">Очистить закрытое хранилище, где хранятся файлы (Файлы можно заново скачать с сервера)</string>
<string name="i_followed_this_link_from_a_trusted_source">Открывать ссылки из надёжного источника</string> <string name="i_followed_this_link_from_a_trusted_source">Открывать ссылки из надёжного источника</string>
<string name="verifying_omemo_keys_trusted_source">Вы потвердите OMEMO ключи %1$s после нажатия на ссылку. Это безопасно только если вы перешли по ссылке из доверенного источника, где только %2$sмог разместить эту ссылку.</string> <string name="verifying_omemo_keys_trusted_source">Вы потвердите OMEMO ключи %1$s после нажатия на ссылку. Это безопасно только если вы перешли по ссылке из доверенного источника, где только %2$sмог разместить эту ссылку.</string>
<string name="verify_omemo_keys">Проверить OMEMO ключ</string> <string name="verify_omemo_keys">Проверить OMEMO-ключи</string>
<string name="show_inactive_devices">Показывать неактивные</string> <string name="show_inactive_devices">Показывать неактивные</string>
<string name="hide_inactive_devices">Скрыть неактивные</string> <string name="hide_inactive_devices">Скрыть неактивные</string>
<string name="distrust_omemo_key">Не доверенное устройство.</string> <string name="distrust_omemo_key">Прекратить доверять устройству</string>
<string name="distrust_omemo_key_text">Вы действительно хотите удалить устройство из доверенных?\Устройство и сообщения, полученные с этого устройства, будут помечаться как недоверенные.</string> <string name="distrust_omemo_key_text">Вы действительно хотите удалить устройство из доверенных?\Устройство и сообщения, полученные с этого устройства, будут помечаться как недоверенные.</string>
<plurals name="seconds"> <plurals name="seconds">
<item quantity="one">%d секунда</item> <item quantity="one">%d секунда</item>
@ -618,7 +647,10 @@
<string name="copy_to_clipboard">Скопировать в буфер обмена</string> <string name="copy_to_clipboard">Скопировать в буфер обмена</string>
<string name="message_copied_to_clipboard">Сообщение скопировано в буфер обмена</string> <string name="message_copied_to_clipboard">Сообщение скопировано в буфер обмена</string>
<string name="message">Сообщение</string> <string name="message">Сообщение</string>
<string name="private_messages_are_disabled">Личные сообщения выключены</string>
<string name="mtm_accept_cert">Принять Неизвестный Сертификат?</string> <string name="mtm_accept_cert">Принять Неизвестный Сертификат?</string>
<string name="pref_scroll_to_bottom">Прокручивать вниз</string>
<string name="pref_scroll_to_bottom_summary">Прокручивать вниз после отправки сообщения</string>
<string name="edit_status_message_title">Редактировать статусное сообщение</string> <string name="edit_status_message_title">Редактировать статусное сообщение</string>
<string name="edit_status_message">Редактировать статусное сообщение</string> <string name="edit_status_message">Редактировать статусное сообщение</string>
<string name="disable_encryption">Отключить шифрование</string> <string name="disable_encryption">Отключить шифрование</string>
@ -626,16 +658,43 @@
<string name="disable_now">Отключить сейчас</string> <string name="disable_now">Отключить сейчас</string>
<string name="draft">Черновик:</string> <string name="draft">Черновик:</string>
<string name="pref_omemo_setting">OMEMO-шифрование</string> <string name="pref_omemo_setting">OMEMO-шифрование</string>
<string name="pref_omemo_setting_summary_always">OMEMO будет всегда использоваться для одиночных бесед и закрытых конференций.</string>
<string name="pref_omemo_setting_summary_default_on">OMEMO будет использоваться по умолчанию для новых бесед.</string>
<string name="pref_omemo_setting_summary_default_off">OMEMO нужно будет явно включать для новых бесед.</string>
<string name="pref_font_size">Размер шрифта</string> <string name="pref_font_size">Размер шрифта</string>
<string name="pref_font_size_summary">Относительный размер шрифта используемый в приложении.</string>
<string name="default_on">Включено по умолчанию</string>
<string name="default_off">Выключено по умолчанию</string>
<string name="small">Маленький</string> <string name="small">Маленький</string>
<string name="medium">Средний</string> <string name="medium">Средний</string>
<string name="large">Большой</string> <string name="large">Большой</string>
<string name="not_encrypted_for_this_device">Сообщение не зашифровано для этого устройства.</string>
<string name="omemo_decryption_failed">Не удалось расшифровать OMEMO-сообщение.</string>
<string name="undo">отменить</string> <string name="undo">отменить</string>
<string name="action_copy_location">Копировать местоположение</string> <string name="action_copy_location">Копировать местоположение</string>
<string name="action_share_location">Поделиться местоположением</string> <string name="action_share_location">Поделиться местоположением</string>
<string name="title_activity_share_location">Поделиться местоположением</string> <string name="title_activity_share_location">Поделиться местоположением</string>
<string name="title_activity_show_location">Показать местоположение</string> <string name="title_activity_show_location">Показать местоположение</string>
<string name="share">Поделиться</string> <string name="share">Поделиться</string>
<string name="unable_to_start_recording">Невозможно начать запись</string>
<string name="please_wait">Пожалуйста, подождите…</string> <string name="please_wait">Пожалуйста, подождите…</string>
<string name="no_microphone_permission">Conversations нужен доступ к микрофону</string>
<string name="search_messages">Поиск сообщений</string> <string name="search_messages">Поиск сообщений</string>
<string name="view_conversation">Посмотреть беседу</string>
<string name="copy_link">Копировать веб-адрес</string>
<string name="copy_jabber_id">Копировать XMPP-адрес</string>
<string name="pref_start_search">Быстрый поиск</string>
<string name="pref_start_search_summary">На экране \"Начать беседу\" открывать клавиатуру и ставить курсор в поле поиска</string>
<string name="pref_more_notification_settings">Настройки уведомлений</string>
<string name="view_media">Просмотр медиа</string>
<string name="pref_video_compression">Качество видео</string>
<string name="pref_video_compression_summary">Низкое качество означает меньшие файлы</string>
<string name="video_360p">Среднее (360p)</string>
<string name="video_720p">Высокое (720р)</string>
<string name="video_original">Оригинал (без сжатия)</string>
<string name="create_group_chat">Создать конференцию</string>
<string name="join_public_channel">Присоединиться к каналу</string>
<string name="create_private_group_chat">Создать закрытую конференцию</string>
<string name="create_public_channel">Создать публичный канал</string>
<string name="discover_channels">Найти каналы</string>
</resources> </resources>

View File

@ -17,6 +17,8 @@
<string name="action_unblock_contact">Unblock contact</string> <string name="action_unblock_contact">Unblock contact</string>
<string name="action_block_domain">Block domain</string> <string name="action_block_domain">Block domain</string>
<string name="action_unblock_domain">Unblock domain</string> <string name="action_unblock_domain">Unblock domain</string>
<string name="action_block_participant">Block participant</string>
<string name="action_unblock_participant">Unblock participant</string>
<string name="title_activity_manage_accounts">Manage Accounts</string> <string name="title_activity_manage_accounts">Manage Accounts</string>
<string name="title_activity_settings">Settings</string> <string name="title_activity_settings">Settings</string>
<string name="title_activity_sharewith">Share with Conversation</string> <string name="title_activity_sharewith">Share with Conversation</string>

View File

@ -5,13 +5,16 @@ import android.util.Log;
import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.common.GoogleApiAvailability;
import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket;
@ -19,82 +22,161 @@ import rocks.xmpp.addr.Jid;
public class PushManagementService { public class PushManagementService {
protected final XmppConnectionService mXmppConnectionService; protected final XmppConnectionService mXmppConnectionService;
PushManagementService(XmppConnectionService service) { PushManagementService(XmppConnectionService service) {
this.mXmppConnectionService = service; this.mXmppConnectionService = service;
} }
void registerPushTokenOnServer(final Account account) { private static Data findResponseData(IqPacket response) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support"); final Element command = response.findChild("command", Namespace.COMMANDS);
retrieveFcmInstanceToken(token -> { final Element x = command == null ? null : command.findChild("x", Namespace.DATA);
final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); return x == null ? null : Data.parse(x);
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 void enablePushOnServer(final Account account, final Jid jid, final String node, final String secret) { private Jid getAppServer() {
IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(jid, node, secret); return Jid.of(mXmppConnectionService.getString(R.string.app_server));
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 retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) { void registerPushTokenOnServer(final Account account) {
new Thread(() -> { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support");
try { retrieveFcmInstanceToken(token -> {
instanceTokenRetrieved.onGcmInstanceTokenRetrieved(FirebaseInstanceId.getInstance().getToken()); final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
} catch (Exception e) { final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId);
Log.d(Config.LOGTAG, "unable to get push token",e); mXmppConnectionService.sendIqPacket(account, packet, (a, response) -> {
} final Data data = findResponseData(response);
}).start(); 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) { public boolean available(Account account) {
final XmppConnection connection = account.getXmppConnection(); final XmppConnection connection = account.getXmppConnection();
return connection != null return connection != null
&& connection.getFeatures().sm() && connection.getFeatures().sm()
&& connection.getFeatures().push() && connection.getFeatures().push()
&& playServicesAvailable(); && playServicesAvailable();
} }
private boolean playServicesAvailable() { private boolean playServicesAvailable() {
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS; return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS;
} }
public boolean isStub() { public boolean isStub() {
return false; return false;
} }
interface OnGcmInstanceTokenRetrieved { interface OnGcmInstanceTokenRetrieved {
void onGcmInstanceTokenRetrieved(String token); void onGcmInstanceTokenRetrieved(String token);
} }
} }

View File

@ -203,7 +203,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
os.close(); os.close();
connection.connect(); connection.connect();
final int code = connection.getResponseCode(); final int code = connection.getResponseCode();
if (code == 200) { if (code == 200 || code == 201) {
account.setOption(Account.OPTION_UNVERIFIED, false); account.setOption(Account.OPTION_UNVERIFIED, false);
account.setOption(Account.OPTION_DISABLED, false); account.setOption(Account.OPTION_DISABLED, false);
awaitingAccountStateChange = new CountDownLatch(1); awaitingAccountStateChange = new CountDownLatch(1);

View File

@ -42,6 +42,9 @@ public class ApiDialogHelper {
case 409: case 409:
res = R.string.logged_in_with_another_device; res = R.string.logged_in_with_another_device;
break; break;
case 451:
res = R.string.not_available_in_your_country;
break;
case 500: case 500:
res = R.string.something_went_wrong_processing_your_request; res = R.string.something_went_wrong_processing_your_request;
break; break;

View File

@ -6,4 +6,4 @@
<string name="pref_broadcast_last_activity_summary">إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي</string> <string name="pref_broadcast_last_activity_summary">إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي</string>
<string name="no_microphone_permission">كويكسي يحتاج الإتصال بالمايكروفون</string> <string name="no_microphone_permission">كويكسي يحتاج الإتصال بالمايكروفون</string>
<string name="set_profile_picture">صورة حساب كويكسي</string> <string name="set_profile_picture">صورة حساب كويكسي</string>
</resources> </resources>

View File

@ -19,4 +19,5 @@
<string name="no_microphone_permission">Quicksy се нуждае от достъп до микрофона</string> <string name="no_microphone_permission">Quicksy се нуждае от достъп до микрофона</string>
<string name="foreground_service_channel_description">Тази категория известия се използва за показване на постоянно известие, което показва, че Quicksy работи.</string> <string name="foreground_service_channel_description">Тази категория известия се използва за показване на постоянно известие, което показва, че Quicksy работи.</string>
<string name="set_profile_picture">Профилна снимка за Quicksy</string> <string name="set_profile_picture">Профилна снимка за Quicksy</string>
<string name="not_available_in_your_country">Quicksy не може да се използва във Вашата страна.</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Quicksy necessita accés al micròfon</string> <string name="no_microphone_permission">Quicksy necessita accés al micròfon</string>
<string name="foreground_service_channel_description">Aquest tipus de notificació s\'utilitza per mostrar una notificació permanent que indica que Quicksy s\'està executant.</string> <string name="foreground_service_channel_description">Aquest tipus de notificació s\'utilitza per mostrar una notificació permanent que indica que Quicksy s\'està executant.</string>
<string name="set_profile_picture">Imatge de perfil en Quicksy</string> <string name="set_profile_picture">Imatge de perfil en Quicksy</string>
</resources> </resources>

View File

@ -19,4 +19,5 @@
<string name="no_microphone_permission">Quicksy benötigt Zugriff auf das Mikrofon</string> <string name="no_microphone_permission">Quicksy benötigt Zugriff auf das Mikrofon</string>
<string name="foreground_service_channel_description">Diese Benachrichtigungsart wird verwendet, um eine permanente Benachrichtigung anzuzeigen, die anzeigt, dass Quicksy gerade ausgeführt wird.</string> <string name="foreground_service_channel_description">Diese Benachrichtigungsart wird verwendet, um eine permanente Benachrichtigung anzuzeigen, die anzeigt, dass Quicksy gerade ausgeführt wird.</string>
<string name="set_profile_picture">Quicksy Profilbild</string> <string name="set_profile_picture">Quicksy Profilbild</string>
<string name="not_available_in_your_country">Quicksy ist in deinem Land nicht verfügbar.</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Quicksy necesita acceder al micrófono</string> <string name="no_microphone_permission">Quicksy necesita acceder al micrófono</string>
<string name="foreground_service_channel_description">Esta categoría de notificación se usa para mostrar una notificación permantente indicando que Quicksy está ejecutándose.</string> <string name="foreground_service_channel_description">Esta categoría de notificación se usa para mostrar una notificación permantente indicando que Quicksy está ejecutándose.</string>
<string name="set_profile_picture">Foto de perfil en Quicksy</string> <string name="set_profile_picture">Foto de perfil en Quicksy</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Quicksy doit avoir accès au microphone</string> <string name="no_microphone_permission">Quicksy doit avoir accès au microphone</string>
<string name="foreground_service_channel_description">Cette catégorie de notification est utilisée pour afficher une notification permanente indiquant que Quicksy est en cours d\'exécution.</string> <string name="foreground_service_channel_description">Cette catégorie de notification est utilisée pour afficher une notification permanente indiquant que Quicksy est en cours d\'exécution.</string>
<string name="set_profile_picture">Photo de profil Quicksy</string> <string name="set_profile_picture">Photo de profil Quicksy</string>
</resources> </resources>

View File

@ -19,4 +19,5 @@
<string name="no_microphone_permission">Quicksy precisa acceder ao micrófono</string> <string name="no_microphone_permission">Quicksy precisa acceder ao micrófono</string>
<string name="foreground_service_channel_description">Esta categoría de notificacións utilízase para mostrar unha notificación permanente que indica que Quicksy está funcionando.</string> <string name="foreground_service_channel_description">Esta categoría de notificacións utilízase para mostrar unha notificación permanente que indica que Quicksy está funcionando.</string>
<string name="set_profile_picture">Imaxe de perfil Quicksy</string> <string name="set_profile_picture">Imaxe de perfil Quicksy</string>
<string name="not_available_in_your_country">Quicksy non está dispoñible no seu país.</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">A Quicksy-nek hozzáférésre lenne szüksége a mikrofonhoz</string> <string name="no_microphone_permission">A Quicksy-nek hozzáférésre lenne szüksége a mikrofonhoz</string>
<string name="foreground_service_channel_description">Ez az értesítési kategória állandó értesítést jelenít meg arról, hogy a Quicksy fut.</string> <string name="foreground_service_channel_description">Ez az értesítési kategória állandó értesítést jelenít meg arról, hogy a Quicksy fut.</string>
<string name="set_profile_picture">Quicksy profilkép</string> <string name="set_profile_picture">Quicksy profilkép</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Quicksy ha bisogno di accedere al microfono</string> <string name="no_microphone_permission">Quicksy ha bisogno di accedere al microfono</string>
<string name="foreground_service_channel_description">Questa categoria di notifiche è usata per mostrare una notifica permanente per indicare che Quicksy è in esecuzione.</string> <string name="foreground_service_channel_description">Questa categoria di notifiche è usata per mostrare una notifica permanente per indicare che Quicksy è in esecuzione.</string>
<string name="set_profile_picture">Immagine profilo di Quicksy</string> <string name="set_profile_picture">Immagine profilo di Quicksy</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Quicksy はマイクにアクセスが必要です</string> <string name="no_microphone_permission">Quicksy はマイクにアクセスが必要です</string>
<string name="foreground_service_channel_description">この通知カテゴリーは Quicksy が実行されていることを表示する、永続的な通知を表示するために使用されます。</string> <string name="foreground_service_channel_description">この通知カテゴリーは Quicksy が実行されていることを表示する、永続的な通知を表示するために使用されます。</string>
<string name="set_profile_picture">Quicksy プロフィール写真</string> <string name="set_profile_picture">Quicksy プロフィール写真</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Quicksy heeft toegang nodig tot de microfoon</string> <string name="no_microphone_permission">Quicksy heeft toegang nodig tot de microfoon</string>
<string name="foreground_service_channel_description">Deze meldingscategorie wordt gebruikt om een permanente melding weer te geven dat Quicksy wordt uitgevoerd.</string> <string name="foreground_service_channel_description">Deze meldingscategorie wordt gebruikt om een permanente melding weer te geven dat Quicksy wordt uitgevoerd.</string>
<string name="set_profile_picture">Quicksy-profielafbeelding</string> <string name="set_profile_picture">Quicksy-profielafbeelding</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Quicksy potrzebuje dostępu do mikrofonu.</string> <string name="no_microphone_permission">Quicksy potrzebuje dostępu do mikrofonu.</string>
<string name="foreground_service_channel_description">Ta kategoria powiadomień jest używana do wyświetlania ciągłego powiadomienia o tym, że Quicksy działa.</string> <string name="foreground_service_channel_description">Ta kategoria powiadomień jest używana do wyświetlania ciągłego powiadomienia o tym, że Quicksy działa.</string>
<string name="set_profile_picture">Obrazek profilowy Quicksy</string> <string name="set_profile_picture">Obrazek profilowy Quicksy</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">O Quicksy necessita de acesso ao microfone</string> <string name="no_microphone_permission">O Quicksy necessita de acesso ao microfone</string>
<string name="foreground_service_channel_description">Essa categoria de notificação é utilizada para exibir uma notificação permanente indicando que o Quicksy está em execução.</string> <string name="foreground_service_channel_description">Essa categoria de notificação é utilizada para exibir uma notificação permanente indicando que o Quicksy está em execução.</string>
<string name="set_profile_picture">Imagem de perfil do Quicksy</string> <string name="set_profile_picture">Imagem de perfil do Quicksy</string>
</resources> </resources>

View File

@ -21,4 +21,5 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați
<string name="no_microphone_permission">Quicksy are nevoie de acces la microfon</string> <string name="no_microphone_permission">Quicksy are nevoie de acces la microfon</string>
<string name="foreground_service_channel_description">Această categorie de notificări este folosită pentru a arăta o notificare permanentă ce indică rularea Quicksy</string> <string name="foreground_service_channel_description">Această categorie de notificări este folosită pentru a arăta o notificare permanentă ce indică rularea Quicksy</string>
<string name="set_profile_picture">Poză profil Quicksy</string> <string name="set_profile_picture">Poză profil Quicksy</string>
<string name="not_available_in_your_country">Quicksy nu este disponibilă în țara dumneavoastră.</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Програма потребує доступу до мікрофона</string> <string name="no_microphone_permission">Програма потребує доступу до мікрофона</string>
<string name="foreground_service_channel_description">Цей вид сповіщень показує постійне сповіщення про те, що ця програма працює.</string> <string name="foreground_service_channel_description">Цей вид сповіщень показує постійне сповіщення про те, що ця програма працює.</string>
<string name="set_profile_picture">Зображення профілю для Quicksy</string> <string name="set_profile_picture">Зображення профілю для Quicksy</string>
</resources> </resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Quicksy需要麦克风权限</string> <string name="no_microphone_permission">Quicksy需要麦克风权限</string>
<string name="foreground_service_channel_description">此通知类别用于显示表明Quicksy正在运行的永久通知。</string> <string name="foreground_service_channel_description">此通知类别用于显示表明Quicksy正在运行的永久通知。</string>
<string name="set_profile_picture">Quicksy个人资料图片</string> <string name="set_profile_picture">Quicksy个人资料图片</string>
</resources> </resources>

View File

@ -19,4 +19,5 @@
<string name="no_microphone_permission">Quicksy needs access to the microphone</string> <string name="no_microphone_permission">Quicksy needs access to the microphone</string>
<string name="foreground_service_channel_description">This notification category is used to display a permanent notification indicating that Quicksy is running.</string> <string name="foreground_service_channel_description">This notification category is used to display a permanent notification indicating that Quicksy is running.</string>
<string name="set_profile_picture">Quicksy profile picture</string> <string name="set_profile_picture">Quicksy profile picture</string>
<string name="not_available_in_your_country">Quicksy is not available in your country.</string>
</resources> </resources>