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
### Version 2.5.4
* stability improvements for group chats and channels
### Version 2.5.3
* bug fixes for peer to peer file transfer (Jingle)
* fixed server info for unlimited/unknown max file size

View File

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

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 java.lang.**
-dontwarn javax.lang.**
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod
# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <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;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
public class PushManagementService {
@ -10,7 +11,19 @@ public class PushManagementService {
this.mXmppConnectionService = service;
}
public void registerPushTokenOnServer(Account account) {
void registerPushTokenOnServer(Account account) {
//stub implementation. only affects playstore flavor
}
void registerPushTokenOnServer(Conversation conversation) {
//stub implementation. only affects playstore flavor
}
void unregisterChannel(Account account, String hash) {
//stub implementation. only affects playstore flavor
}
void disablePushOnServer(Conversation conversation) {
//stub implementation. only affects playstore flavor
}

View File

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

View File

@ -64,8 +64,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
protected final JSONObject keys;
private final Roster roster = new Roster(this);
private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>();
public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>();
public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
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 String password;
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_ALWAYS_NOTIFY = "always_notify";
public static final String ATTRIBUTE_PUSH_NODE = "push_node";
public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";

View File

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

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) {
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
final Element block = iq.addChild("block", Namespace.BLOCKING);
final Element item = block.addChild("item").setAttribute("jid", jid.asBareJid().toString());
final Element item = block.addChild("item").setAttribute("jid", jid.toEscapedString());
if (reportSpam) {
item.addChild("report", "urn:xmpp:reporting:0").addChild("spam");
}
@ -316,7 +316,7 @@ public class IqGenerator extends AbstractGenerator {
public IqPacket generateSetUnblockRequest(final Jid jid) {
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
final Element block = iq.addChild("unblock", Namespace.BLOCKING);
block.addChild("item").setAttribute("jid", jid.asBareJid().toString());
block.addChild("item").setAttribute("jid", jid.toEscapedString());
return iq;
}
@ -423,29 +423,60 @@ public class IqGenerator extends AbstractGenerator {
}
public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
return pushTokenToAppServer(appServer, token, deviceId, null);
}
public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
packet.setTo(appServer);
Element command = packet.addChild("command", "http://jabber.org/protocol/commands");
final Element command = packet.addChild("command", Namespace.COMMANDS);
command.setAttribute("node", "register-push-fcm");
command.setAttribute("action", "execute");
Data data = new Data();
final Data data = new Data();
data.put("token", token);
data.put("android-id", deviceId);
if (muc != null) {
data.put("muc", muc.toEscapedString());
}
data.submit();
command.addChild(data);
return packet;
}
public IqPacket unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
packet.setTo(appServer);
final Element command = packet.addChild("command", Namespace.COMMANDS);
command.setAttribute("node", "unregister-push-fcm");
command.setAttribute("action", "execute");
final Data data = new Data();
data.put("channel", channel);
data.put("android-id", deviceId);
data.submit();
command.addChild(data);
return packet;
}
public IqPacket enablePush(Jid jid, String node, String secret) {
public IqPacket enablePush(final Jid jid, final String node, final String secret) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
Element enable = packet.addChild("enable", "urn:xmpp:push:0");
Element enable = packet.addChild("enable", Namespace.PUSH);
enable.setAttribute("jid", jid.toString());
enable.setAttribute("node", node);
Data data = new Data();
data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS);
data.put("secret", secret);
data.submit();
enable.addChild(data);
if (secret != null) {
Data data = new Data();
data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS);
data.put("secret", secret);
data.submit();
enable.addChild(data);
}
return packet;
}
public IqPacket disablePush(final Jid jid, final String node) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
Element disable = packet.addChild("disable", Namespace.PUSH);
disable.setAttribute("jid", jid.toEscapedString());
disable.setAttribute("node", node);
return packet;
}

View File

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

View File

@ -22,6 +22,7 @@ import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.ReadByMarker;
@ -126,7 +127,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
service.reportBrokenSessionException(e, postpone);
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
} else {
Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicase failed");
Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicates failed");
//TODO should be still emit a failed message?
return null;
}
} catch (NotEncryptedForThisDeviceException e) {
@ -264,6 +266,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
packet.getId(),
Message.STATUS_SEND_FAILED,
extractErrorMessage(packet));
final Element error = packet.findChild("error");
final boolean pingWorthyError = error != null && (error.hasChild("not-acceptable") || error.hasChild("remote-server-timeout") || error.hasChild("remote-server-not-found"));
if (pingWorthyError) {
Conversation conversation = mXmppConnectionService.find(account,from);
if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) {
if (conversation.getMucOptions().online()) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received ping worthy error for seemingly online muc at "+from);
mXmppConnectionService.mucSelfPingAndRejoin(conversation);
}
}
}
}
return true;
}
@ -437,6 +450,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
origin = from;
}
//TODO either or is probably fine?
final boolean checkedForDuplicates = serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId);
if (origin != null) {
@ -598,7 +612,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} else {
serverMsgIdUpdated = false;
}
Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + Boolean.toString(serverMsgIdUpdated));
Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + serverMsgIdUpdated);
return;
}
}

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

View File

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

View File

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

View File

@ -28,14 +28,18 @@ public final class BlockContactDialog {
final String value;
@StringRes int res;
if (blockable.getJid().getLocal() == null || blockable.getAccount().isBlocked(Jid.ofDomain(blockable.getJid().getDomain()))) {
if (blockable.getJid().isFullJid()) {
builder.setTitle(isBlocked ? R.string.action_unblock_participant : R.string.action_block_participant);
value = blockable.getJid().toEscapedString();
res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text;
} else if (blockable.getJid().getLocal() == null || blockable.getAccount().isBlocked(Jid.ofDomain(blockable.getJid().getDomain()))) {
builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain);
value = Jid.ofDomain(blockable.getJid().getDomain()).toString();
res = isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text;
} else {
int resBlockAction = blockable instanceof Conversation && ((Conversation) blockable).isWithStranger() ? R.string.block_stranger : R.string.action_block_contact;
builder.setTitle(isBlocked ? R.string.action_unblock_contact : resBlockAction);
value = blockable.getJid().asBareJid().toString();
value = blockable.getJid().asBareJid().toEscapedString();
res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text;
}
binding.text.setText(JidDialog.style(xmppActivity, res, value));

View File

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

View File

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

View File

@ -632,7 +632,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
@Override
public void userInputRequried(PendingIntent pi, Message object) {
public void userInputRequired(PendingIntent pi, Message object) {
}
});
@ -666,7 +666,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
@Override
public void userInputRequried(PendingIntent pi, Message message) {
public void userInputRequired(PendingIntent pi, Message message) {
hidePrepareFileToast(prepareFileToast);
}
});
@ -688,7 +688,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi, Message object) {
public void userInputRequired(PendingIntent pi, Message object) {
hidePrepareFileToast(prepareFileToast);
}
@ -1326,7 +1326,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
new UiCallback<Contact>() {
@Override
public void userInputRequried(PendingIntent pi, Contact contact) {
public void userInputRequired(PendingIntent pi, Contact contact) {
startPendingIntent(pi, attachmentChoice);
}
@ -2284,7 +2284,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
status = Presence.Status.OFFLINE;
}
this.binding.textSendButton.setTag(action);
this.binding.textSendButton.setImageResource(SendButtonTool.getSendButtonImageResource(getActivity(), action, status));
final Activity activity = getActivity();
if (activity != null) {
this.binding.textSendButton.setImageResource(SendButtonTool.getSendButtonImageResource(activity, action, status));
}
}
protected void updateStatusMessages() {
@ -2456,7 +2459,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
new UiCallback<Contact>() {
@Override
public void userInputRequried(PendingIntent pi, Contact contact) {
public void userInputRequired(PendingIntent pi, Contact contact) {
startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE);
}
@ -2512,7 +2515,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi, Message message) {
public void userInputRequired(PendingIntent pi, Message 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>() {
@Override
public void userInputRequried(final PendingIntent pi, final Avatar avatar) {
public void userInputRequired(final PendingIntent pi, final Avatar avatar) {
finishInitialSetup(avatar);
}
@ -917,7 +917,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
}
@Override
public void userInputRequried(PendingIntent pi, String object) {
public void userInputRequired(PendingIntent pi, String object) {
mPendingPresenceTemplate.push(template);
try {
startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0);

View File

@ -23,8 +23,10 @@ import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController;
import org.osmdroid.config.Configuration;
import org.osmdroid.config.IConfigurationProvider;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.tileprovider.tilesource.XYTileSource;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.CustomZoomButtonsController;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.Overlay;
@ -72,15 +74,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
protected void updateLocationMarkers() {
clearMarkers();
}
protected XYTileSource tileSource() {
return new XYTileSource("OpenStreetMap",
0, 19, 256, ".png", new String[] {
"https://a.tile.openstreetmap.org/",
"https://b.tile.openstreetmap.org/",
"https://c.tile.openstreetmap.org/" },"© OpenStreetMap contributors");
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -103,7 +97,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
final IConfigurationProvider config = Configuration.getInstance();
config.load(ctx, getPreferences());
config.setUserAgentValue(BuildConfig.APPLICATION_ID + "_" + BuildConfig.VERSION_CODE);
config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE);
if (QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor)) {
try {
config.setHttpProxy(HttpConnectionManager.getProxy());
@ -111,17 +105,6 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
throw new RuntimeException("Unable to configure proxy");
}
}
final File f = new File(ctx.getCacheDir() + "/tiles");
try {
//noinspection ResultOfMethodCallIgnored
f.mkdirs();
} catch (final SecurityException ignored) {
}
if (f.exists() && f.isDirectory() && f.canRead() && f.canWrite()) {
Log.d(Config.LOGTAG, "Using tile cache at: " + f.getAbsolutePath());
config.setOsmdroidTileCache(f.getAbsoluteFile());
}
}
@Override
@ -150,8 +133,8 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
protected void setupMapView(MapView mapView, final GeoPoint pos) {
map = mapView;
map.setTileSource(tileSource());
map.setBuiltInZoomControls(false);
map.setTileSource(TileSourceFactory.MAPNIK);
map.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
map.setMultiTouchControls(true);
map.setTilesScaledToDpi(true);
mapController = map.getController();
@ -251,7 +234,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
requestLocationUpdates();
updateLocationMarkers();
updateUi();
map.setTileSource(tileSource());
map.setTileSource(TileSourceFactory.MAPNIK);
map.setTilesScaledToDpi(true);
if (mapAtInitialLoc()) {

View File

@ -174,7 +174,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
@Override
public void userInputRequried(PendingIntent pi, Conversation object) {
public void userInputRequired(PendingIntent pi, Conversation object) {
}
};
@ -1085,7 +1085,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
@Override
public void userInputRequried(PendingIntent pi, Conversation object) {
public void userInputRequired(PendingIntent pi, Conversation object) {
}
});

View File

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

View File

@ -246,8 +246,12 @@ public final class CryptoHelper {
return prettifyFingerprintCert(bytesToHex(fingerprint));
}
public static String getFingerprint(Jid jid, String androidId) {
return getFingerprint(jid.toEscapedString() + "\00" + androidId);
}
public static String getAccountFingerprint(Account account, String androidId) {
return getFingerprint(account.getJid().asBareJid().toEscapedString() + "\00" + androidId);
return getFingerprint(account.getJid().asBareJid(), androidId);
}
public static String getFingerprint(String value) {

View File

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

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

View File

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

View File

@ -1439,15 +1439,8 @@ public class XmppConnection implements Runnable {
}
private void forceCloseSocket() {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception " + e.getMessage() + " during force close");
}
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": socket was null during force close");
}
FileBackend.close(this.socket);
FileBackend.close(this.tagReader);
}
public void interrupt() {
@ -1458,7 +1451,7 @@ public class XmppConnection implements Runnable {
public void disconnect(final boolean force) {
interrupt();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + Boolean.toString(force));
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": disconnecting force=" + force);
if (force) {
forceCloseSocket();
} else {
@ -1798,8 +1791,8 @@ public class XmppConnection implements Runnable {
}
public boolean push() {
return hasDiscoFeature(account.getJid().asBareJid(), "urn:xmpp:push:0")
|| hasDiscoFeature(Jid.of(account.getServer()), "urn:xmpp:push:0");
return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUSH)
|| hasDiscoFeature(Jid.of(account.getServer()), Namespace.PUSH);
}
public boolean rosterVersioning() {

View File

@ -776,4 +776,10 @@
<string name="open_with">Отваряне с…</string>
<string name="set_profile_picture">Профилна снимка за Conversations</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>

View File

@ -17,6 +17,8 @@
<string name="action_unblock_contact">Kontakt entsperren</string>
<string name="action_block_domain">Domain sperren</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_settings">Einstellungen</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_block_domain">Bloquear 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_settings">Axustes</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_block_domain">Blochează 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_settings">Setări</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_add">Новая беседа</string>
<string name="action_accounts">Управление аккаунтами</string>
<string name="action_end_conversation">Закрыть текущую беседу</string>
<string name="action_contact_details">Сведения о контакте</string>
<string name="action_muc_details">Подробности конференции</string>
<string name="channel_details">Сведения о канале</string>
<string name="action_secure">Защищённая беседа</string>
<string name="action_add_account">Добавить аккаунт</string>
<string name="action_edit_contact">Редактировать контакт</string>
@ -48,6 +50,7 @@
<string name="share_with">Поделиться с</string>
<string name="start_conversation">Начать беседу</string>
<string name="invite_contact">Пригласить собеседника</string>
<string name="invite">Пригласить</string>
<string name="contacts">Контакты</string>
<string name="contact">Контакт</string>
<string name="cancel">Отмена</string>
@ -78,6 +81,7 @@
<string name="clear_histor_msg">Вы хотите удалить все сообщения в этой беседе?\n\n<b>Предупреждение:</b> Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах.</string>
<string name="delete_file_dialog">Удалить файл</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="send_unencrypted_message">Нешифрованное сообщение</string>
<string name="send_message">Сообщение</string>
@ -158,15 +162,18 @@
<string name="mgmt_account_disable">Временно отключить</string>
<string name="mgmt_account_publish_avatar">Разместить аватар</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="openpgp_has_been_published">Открытый ключ OpenPGP был опубликован.</string>
<string name="mgmt_account_enable">Включить аккаунт</string>
<string name="mgmt_account_are_you_sure">Вы уверены?</string>
<string name="mgmt_account_delete_confirm_text">Если вы удалите аккаунт, будет потеряна вся история переписки.</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="password">Пароль</string>
<string name="invalid_jid">Недопустимый XMPP-адрес</string>
<string name="error_out_of_memory">Недостаточно памяти. Изображение слишком большое</string>
<string name="add_phone_book_text">Вы хотите добавить %s в вашу адресную книгу?</string>
<string name="server_info_show_more">Информация о сервере</string>
@ -201,6 +208,7 @@
<string name="fetching_keys">Получение ключей…</string>
<string name="done">Готово</string>
<string name="decrypt">Расшифровать</string>
<string name="bookmarks">Закладки</string>
<string name="search">Поиск</string>
<string name="enter_contact">Добавить контакт</string>
<string name="delete_contact">Удалить контакт</string>
@ -213,6 +221,8 @@
<string name="join">Присоединиться</string>
<string name="save_as_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="action_edit_subject">Редактировать тему конференции</string>
<string name="topic">Тема</string>
@ -253,6 +263,7 @@
<string name="pref_allow_message_correction_summary">Позволить контактам редактировать сообщения</string>
<string name="pref_expert_options">Расширенные настройки</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_start_time">Начало</string>
<string name="title_pref_quiet_hours_end_time">Окончание</string>
@ -281,6 +292,9 @@
<string name="send_again">Отправить ещё раз</string>
<string name="file_url">URL файла</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="show_qr_code">Показать 2D штрихкод</string>
<string name="show_block_list">Показать чёрный список</string>
@ -289,6 +303,14 @@
<string name="try_again">Повторить</string>
<string name="pref_keep_foreground_service">Оставить службу на переднем плане</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="receiving_x_file">%1$s загружается (%2$d%% выполнено)</string>
<string name="download_x_file">Загрузить %s</string>
@ -311,7 +333,7 @@
<string name="conference_creation_failed">Не удалось создать конференцию!</string>
<string name="account_image_description">Аватар аккаунта</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_desc">Вы уверены, что хотите очистить все остальные устройства из анонса ключей OMEMO? При соединении устройств в следующий раз новые ключи анонсируются автоматически, но устройства могут не получить сообщения, посланные до этого.</string>
<string name="error_no_keys_to_trust_server_error">Для этого контакта не существует доступных ключей.\nПопытка получения новых ключей с сервера оказалась неудачной. Возможно, что-то не так с сервером вашего собеседника.</string>
@ -366,8 +388,8 @@
<string name="hide_offline">Скрыть пользователей вне сети</string>
<string name="contact_is_typing">%s печатает…</string>
<string name="contact_has_stopped_typing">%s прекратил набор</string>
<string name="contacts_are_typing">%s набирает...</string>
<string name="contacts_have_stopped_typing">%s перестал печатать</string>
<string name="contacts_are_typing">%s печатают...</string>
<string name="contacts_have_stopped_typing">%s перестали печатать</string>
<string name="pref_chat_states">Оповещения о наборе</string>
<string name="pref_chat_states_summary">Позволяет вашим контактам видеть когда вы пишете им новое сообщение</string>
<string name="send_location">Отправить местоположение</string>
@ -395,7 +417,8 @@
<string name="recently_used">Последнее выбранное</string>
<string name="choose_quick_action">Выбрать быстрое действие</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="username">Имя пользователя</string>
<string name="username_hint">Имя пользователя</string>
@ -409,8 +432,8 @@
<string name="account_status_host_unknown">Сервер не ответственен за домен</string>
<string name="server_info_broken">Повреждено</string>
<string name="pref_presence_settings">Доступность</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">\"Отошёл\" когда экран выключен</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_summary">Помечать ресурс как \"Не беспокоить\", когда устройство в беззвучном режиме</string>
<string name="pref_treat_vibrate_as_silent">Не доступен в режиме вибрации</string>
@ -430,7 +453,7 @@
<string name="certificate_chain_is_not_trusted">Цепочка сертификата не доверена</string>
<string name="action_renew_certificate">Обновить сертификат</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="pref_connection_options">Подключение</string>
<string name="pref_use_tor">Соединение через Tor</string>
@ -459,6 +482,8 @@
<string name="notify_only_when_highlighted">Уведомлять только при упоминании</string>
<string name="notify_never">Без уведомления</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="automatically">Автоматически</string>
<string name="battery_optimizations_enabled">Оптимизации энергопотребления разрешены</string>
@ -475,9 +500,12 @@
<string name="security_error_invalid_file_access">Ошибка безопасности: недействительный доступ к файлу</string>
<string name="no_application_to_share_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="use_own_provider">Использовать свой провайдер</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="presence_chat">Свободен для общения</string>
<string name="presence_online">В сети</string>
@ -491,6 +519,7 @@
<string name="choose_participants">Выбрать участников</string>
<string name="creating_conference">Создание конференции…</string>
<string name="invite_again">Пригласить ещё раз</string>
<string name="gp_disable">Выключен</string>
<string name="gp_short">Короткий</string>
<string name="gp_medium">Средний</string>
<string name="gp_long">Длинный</string>
@ -506,7 +535,7 @@
<string name="type_pc">Компьютер</string>
<string name="type_phone">Телефон</string>
<string name="type_tablet">Планшет</string>
<string name="type_web">Веб браузер</string>
<string name="type_web">Веб-браузер</string>
<string name="type_console">Консоль</string>
<string name="payment_required">Требуется оплата</string>
<string name="missing_internet_permission">Доступ в интернет запрещён</string>
@ -548,10 +577,10 @@
<string name="pref_clean_private_storage_summary">Очистить закрытое хранилище, где хранятся файлы (Файлы можно заново скачать с сервера)</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="verify_omemo_keys">Проверить OMEMO ключ</string>
<string name="verify_omemo_keys">Проверить OMEMO-ключи</string>
<string name="show_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>
<plurals name="seconds">
<item quantity="one">%d секунда</item>
@ -618,7 +647,10 @@
<string name="copy_to_clipboard">Скопировать в буфер обмена</string>
<string name="message_copied_to_clipboard">Сообщение скопировано в буфер обмена</string>
<string name="message">Сообщение</string>
<string name="private_messages_are_disabled">Личные сообщения выключены</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">Редактировать статусное сообщение</string>
<string name="disable_encryption">Отключить шифрование</string>
@ -626,16 +658,43 @@
<string name="disable_now">Отключить сейчас</string>
<string name="draft">Черновик:</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_summary">Относительный размер шрифта используемый в приложении.</string>
<string name="default_on">Включено по умолчанию</string>
<string name="default_off">Выключено по умолчанию</string>
<string name="small">Маленький</string>
<string name="medium">Средний</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="action_copy_location">Копировать местоположение</string>
<string name="action_share_location">Поделиться местоположением</string>
<string name="title_activity_share_location">Поделиться местоположением</string>
<string name="title_activity_show_location">Показать местоположение</string>
<string name="share">Поделиться</string>
<string name="unable_to_start_recording">Невозможно начать запись</string>
<string name="please_wait">Пожалуйста, подождите…</string>
<string name="no_microphone_permission">Conversations нужен доступ к микрофону</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>

View File

@ -17,6 +17,8 @@
<string name="action_unblock_contact">Unblock contact</string>
<string name="action_block_domain">Block 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_settings">Settings</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.GoogleApiAvailability;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
@ -19,82 +22,161 @@ import rocks.xmpp.addr.Jid;
public class PushManagementService {
protected final XmppConnectionService mXmppConnectionService;
protected final XmppConnectionService mXmppConnectionService;
PushManagementService(XmppConnectionService service) {
this.mXmppConnectionService = service;
}
PushManagementService(XmppConnectionService service) {
this.mXmppConnectionService = service;
}
void registerPushTokenOnServer(final Account account) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support");
retrieveFcmInstanceToken(token -> {
final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
final Jid appServer = Jid.of(mXmppConnectionService.getString(R.string.app_server));
IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(appServer, token, androidId);
mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> {
Element command = p.findChild("command", "http://jabber.org/protocol/commands");
if (p.getType() == IqPacket.TYPE.RESULT && command != null) {
Element x = command.findChild("x", Namespace.DATA);
if (x != null) {
Data data = Data.parse(x);
try {
String node = data.getValue("node");
String secret = data.getValue("secret");
Jid jid = Jid.of(data.getValue("jid"));
if (node != null && secret != null) {
enablePushOnServer(a, jid, node, secret);
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
} else {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server");
}
});
});
}
private static Data findResponseData(IqPacket response) {
final Element command = response.findChild("command", Namespace.COMMANDS);
final Element x = command == null ? null : command.findChild("x", Namespace.DATA);
return x == null ? null : Data.parse(x);
}
private void enablePushOnServer(final Account account, final Jid jid, final String node, final String secret) {
IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(jid, node, secret);
mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> {
if (p.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on server");
} else if (p.getType() == IqPacket.TYPE.ERROR) {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on server failed");
}
});
}
private Jid getAppServer() {
return Jid.of(mXmppConnectionService.getString(R.string.app_server));
}
private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) {
new Thread(() -> {
try {
instanceTokenRetrieved.onGcmInstanceTokenRetrieved(FirebaseInstanceId.getInstance().getToken());
} catch (Exception e) {
Log.d(Config.LOGTAG, "unable to get push token",e);
}
}).start();
void registerPushTokenOnServer(final Account account) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support");
retrieveFcmInstanceToken(token -> {
final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId);
mXmppConnectionService.sendIqPacket(account, packet, (a, response) -> {
final Data data = findResponseData(response);
if (response.getType() == IqPacket.TYPE.RESULT && data != null) {
try {
String node = data.getValue("node");
String secret = data.getValue("secret");
Jid jid = Jid.of(data.getValue("jid"));
if (node != null && secret != null) {
enablePushOnServer(a, jid, node, secret);
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
} else {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server");
}
});
});
}
}
public void unregisterChannel(final Account account, final String channel) {
final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
final Jid appServer = getAppServer();
final IqPacket packet = mXmppConnectionService.getIqGenerator().unregisterChannelOnAppServer(appServer, androidId, channel);
mXmppConnectionService.sendIqPacket(account, packet, (a, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG,a.getJid().asBareJid()+": successfully unregistered channel");
} else if (response.getType() == IqPacket.TYPE.ERROR) {
Log.d(Config.LOGTAG, a.getJid().asBareJid()+": unable to unregister channel with hash "+channel);
}
});
}
void registerPushTokenOnServer(final Conversation conversation) {
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": room "+conversation.getJid().asBareJid()+" has push support");
retrieveFcmInstanceToken(token -> {
final Jid muc = conversation.getJid().asBareJid();
final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService);
final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId, muc);
packet.setTo(muc);
mXmppConnectionService.sendIqPacket(conversation.getAccount(), packet, (a, response) -> {
final Data data = findResponseData(response);
if (response.getType() == IqPacket.TYPE.RESULT && data != null) {
try {
final String node = data.getValue("node");
final String secret = data.getValue("secret");
final Jid jid = Jid.of(data.getValue("jid"));
if (node != null && secret != null) {
enablePushOnServer(conversation, jid, node, secret);
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
} else {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server");
}
});
});
}
private void enablePushOnServer(final Account account, final Jid appServer, final String node, final String secret) {
final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret);
mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> {
if (p.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on server");
} else if (p.getType() == IqPacket.TYPE.ERROR) {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on server failed");
}
});
}
private void enablePushOnServer(final Conversation conversation, final Jid appServer, final String node, final String secret) {
final Jid muc = conversation.getJid().asBareJid();
final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret);
enable.setTo(muc);
mXmppConnectionService.sendIqPacket(conversation.getAccount(), enable, (a, p) -> {
if (p.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on " + muc);
if (conversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, node)) {
mXmppConnectionService.updateConversation(conversation);
}
} else if (p.getType() == IqPacket.TYPE.ERROR) {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on " + muc + " failed");
}
});
}
public void disablePushOnServer(final Conversation conversation) {
final Jid muc = conversation.getJid().asBareJid();
final String node = conversation.getAttribute(Conversation.ATTRIBUTE_PUSH_NODE);
if (node != null) {
final IqPacket disable = mXmppConnectionService.getIqGenerator().disablePush(getAppServer(), node);
disable.setTo(muc);
mXmppConnectionService.sendIqPacket(conversation.getAccount(), disable, (account, response) -> {
if (response.getType() == IqPacket.TYPE.ERROR) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to disable push for room "+muc);
}
});
} else {
Log.d(Config.LOGTAG,conversation.getAccount().getJid().asBareJid()+": room "+muc+" has no stored node. unable to disable push");
}
}
private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) {
FirebaseInstanceId.getInstance().getInstanceId().addOnCompleteListener(task -> {
if (!task.isSuccessful()) {
Log.d(Config.LOGTAG, "unable to get Firebase instance token", task.getException());
}
final InstanceIdResult result = task.getResult();
if (result != null) {
instanceTokenRetrieved.onGcmInstanceTokenRetrieved(result.getToken());
}
});
}
public boolean available(Account account) {
final XmppConnection connection = account.getXmppConnection();
return connection != null
&& connection.getFeatures().sm()
&& connection.getFeatures().push()
&& playServicesAvailable();
}
public boolean available(Account account) {
final XmppConnection connection = account.getXmppConnection();
return connection != null
&& connection.getFeatures().sm()
&& connection.getFeatures().push()
&& playServicesAvailable();
}
private boolean playServicesAvailable() {
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS;
}
private boolean playServicesAvailable() {
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS;
}
public boolean isStub() {
return false;
}
public boolean isStub() {
return false;
}
interface OnGcmInstanceTokenRetrieved {
void onGcmInstanceTokenRetrieved(String token);
}
interface OnGcmInstanceTokenRetrieved {
void onGcmInstanceTokenRetrieved(String token);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -19,4 +19,4 @@
<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="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="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="not_available_in_your_country">Quicksy ist in deinem Land nicht verfügbar.</string>
</resources>

View File

@ -19,4 +19,4 @@
<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="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="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>
</resources>
</resources>

View File

@ -19,4 +19,5 @@
<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="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>

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="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>
</resources>
</resources>

View File

@ -19,4 +19,4 @@
<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="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="foreground_service_channel_description">この通知カテゴリーは 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="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>
</resources>
</resources>

View File

@ -19,4 +19,4 @@
<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="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="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>
</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="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="not_available_in_your_country">Quicksy nu este disponibilă în țara dumneavoastră.</string>
</resources>

View File

@ -19,4 +19,4 @@
<string name="no_microphone_permission">Програма потребує доступу до мікрофона</string>
<string name="foreground_service_channel_description">Цей вид сповіщень показує постійне сповіщення про те, що ця програма працює.</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="foreground_service_channel_description">此通知类别用于显示表明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="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="not_available_in_your_country">Quicksy is not available in your country.</string>
</resources>