Merge tag '2.5.4' into develop
This commit is contained in:
commit
415a105b41
|
@ -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
|
||||
|
|
14
build.gradle
14
build.gradle
|
@ -51,7 +51,7 @@ dependencies {
|
|||
conversationsFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion"
|
||||
quicksyFreeCompatImplementation "com.android.support:support-emoji-bundled:$supportLibVersion"
|
||||
implementation 'org.bouncycastle:bcmail-jdk15on:1.58'
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'com.google.zxing:core:3.4.0'
|
||||
implementation 'de.measite.minidns:minidns-hla:0.2.4'
|
||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
implementation 'org.whispersystems:signal-protocol-java:2.6.2'
|
||||
|
@ -59,13 +59,13 @@ dependencies {
|
|||
implementation "com.wefika:flowlayout:0.4.1"
|
||||
implementation 'net.ypresto.androidtranscoder:android-transcoder:0.3.0'
|
||||
implementation project(':libs:xmpp-addr')
|
||||
implementation 'org.osmdroid:osmdroid-android:6.0.3'
|
||||
implementation 'org.osmdroid:osmdroid-android:6.1.0'
|
||||
implementation 'org.hsluv:hsluv:0.2'
|
||||
implementation 'org.conscrypt:conscrypt-android:1.3.0'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.1.0'
|
||||
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||
implementation "com.leinardi.android:speed-dial:2.0.1"
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
|
||||
implementation 'com.google.guava:guava:27.1-android'
|
||||
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.10.1'
|
||||
}
|
||||
|
@ -81,8 +81,8 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 25
|
||||
versionCode 330
|
||||
versionName "2.5.3"
|
||||
versionCode 333
|
||||
versionName "2.5.4"
|
||||
archivesBaseName += "-$versionName"
|
||||
applicationId "eu.sum7.conversations"
|
||||
resValue "string", "applicationId", applicationId
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -73,14 +75,6 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
|
|||
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()) {
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue