Merge tag '2.5.8' into develop
This commit is contained in:
commit
fa0452761f
|
@ -1,5 +1,10 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Version 2.5.8
|
||||||
|
* fixed connection issues over Tor
|
||||||
|
* P2P file transfer (Jingle) now offers direct candidates
|
||||||
|
* Support XEP-0396: Jingle Encrypted Transports - OMEMO
|
||||||
|
|
||||||
### Version 2.5.7
|
### Version 2.5.7
|
||||||
* fixed crash when scanning QR codes on Android 6 and lower
|
* fixed crash when scanning QR codes on Android 6 and lower
|
||||||
* when sharing a message from and to Conversations insert it as quote
|
* when sharing a message from and to Conversations insert it as quote
|
||||||
|
|
|
@ -83,8 +83,8 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 25
|
||||||
versionCode 337
|
versionCode 338
|
||||||
versionName "2.5.7"
|
versionName "2.5.8"
|
||||||
archivesBaseName += "-$versionName"
|
archivesBaseName += "-$versionName"
|
||||||
applicationId "eu.sum7.conversations"
|
applicationId "eu.sum7.conversations"
|
||||||
resValue "string", "applicationId", applicationId
|
resValue "string", "applicationId", applicationId
|
||||||
|
|
|
@ -101,6 +101,7 @@ public final class Config {
|
||||||
|
|
||||||
|
|
||||||
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
|
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
|
||||||
|
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
|
||||||
public static final boolean DISABLE_HTTP_UPLOAD = false;
|
public static final boolean DISABLE_HTTP_UPLOAD = false;
|
||||||
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
|
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
|
||||||
public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background
|
public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background
|
||||||
|
|
|
@ -29,6 +29,8 @@ public abstract class AbstractGenerator {
|
||||||
Content.Version.FT_5.getNamespace(),
|
Content.Version.FT_5.getNamespace(),
|
||||||
Namespace.JINGLE_TRANSPORTS_S5B,
|
Namespace.JINGLE_TRANSPORTS_S5B,
|
||||||
Namespace.JINGLE_TRANSPORTS_IBB,
|
Namespace.JINGLE_TRANSPORTS_IBB,
|
||||||
|
Namespace.JINGLE_ENCRYPTED_TRANSPORT,
|
||||||
|
Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO,
|
||||||
"http://jabber.org/protocol/muc",
|
"http://jabber.org/protocol/muc",
|
||||||
"jabber:x:conference",
|
"jabber:x:conference",
|
||||||
Namespace.OOB,
|
Namespace.OOB,
|
||||||
|
|
|
@ -39,6 +39,7 @@ import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.security.DigestOutputStream;
|
import java.security.DigestOutputStream;
|
||||||
|
@ -359,6 +360,15 @@ public class FileBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void close(final ServerSocket socket) {
|
||||||
|
if (socket != null) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean weOwnFile(Context context, Uri uri) {
|
public static boolean weOwnFile(Context context, Uri uri) {
|
||||||
if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
|
if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -26,6 +26,7 @@ import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.entities.DownloadableFile;
|
import eu.siacs.conversations.entities.DownloadableFile;
|
||||||
import eu.siacs.conversations.utils.Compatibility;
|
import eu.siacs.conversations.utils.Compatibility;
|
||||||
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
|
||||||
public class AbstractConnectionManager {
|
public class AbstractConnectionManager {
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ import android.content.SharedPreferences;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.graphics.drawable.Icon;
|
|
||||||
import android.media.AudioAttributes;
|
import android.media.AudioAttributes;
|
||||||
import android.media.RingtoneManager;
|
import android.media.RingtoneManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
@ -23,7 +22,6 @@ import android.support.v4.app.NotificationCompat;
|
||||||
import android.support.v4.app.NotificationCompat.BigPictureStyle;
|
import android.support.v4.app.NotificationCompat.BigPictureStyle;
|
||||||
import android.support.v4.app.NotificationCompat.Builder;
|
import android.support.v4.app.NotificationCompat.Builder;
|
||||||
import android.support.v4.app.NotificationManagerCompat;
|
import android.support.v4.app.NotificationManagerCompat;
|
||||||
import android.support.v4.app.NotificationCompat.CarExtender.UnreadConversation;
|
|
||||||
import android.support.v4.app.Person;
|
import android.support.v4.app.Person;
|
||||||
import android.support.v4.app.RemoteInput;
|
import android.support.v4.app.RemoteInput;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
|
@ -32,7 +30,6 @@ import android.text.SpannableString;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -64,7 +61,6 @@ import eu.siacs.conversations.utils.Compatibility;
|
||||||
import eu.siacs.conversations.utils.GeoHelper;
|
import eu.siacs.conversations.utils.GeoHelper;
|
||||||
import eu.siacs.conversations.utils.UIHelper;
|
import eu.siacs.conversations.utils.UIHelper;
|
||||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||||
import rocks.xmpp.addr.Jid;
|
|
||||||
|
|
||||||
public class NotificationService {
|
public class NotificationService {
|
||||||
|
|
||||||
|
@ -469,10 +465,7 @@ public class NotificationService {
|
||||||
private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
|
private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
|
||||||
final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
|
final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
|
||||||
final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
|
final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
|
||||||
style.setBigContentTitle(notifications.size()
|
style.setBigContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations,notifications.size()));
|
||||||
+ " "
|
|
||||||
+ mXmppConnectionService
|
|
||||||
.getString(R.string.unread_conversations));
|
|
||||||
final StringBuilder names = new StringBuilder();
|
final StringBuilder names = new StringBuilder();
|
||||||
Conversation conversation = null;
|
Conversation conversation = null;
|
||||||
for (final ArrayList<Message> messages : notifications.values()) {
|
for (final ArrayList<Message> messages : notifications.values()) {
|
||||||
|
@ -497,10 +490,8 @@ public class NotificationService {
|
||||||
if (names.length() >= 2) {
|
if (names.length() >= 2) {
|
||||||
names.delete(names.length() - 2, names.length());
|
names.delete(names.length() - 2, names.length());
|
||||||
}
|
}
|
||||||
mBuilder.setContentTitle(notifications.size()
|
mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations, notifications.size()));
|
||||||
+ " "
|
mBuilder.setTicker(mXmppConnectionService.getString(R.string.x_unread_conversations, notifications.size()));
|
||||||
+ mXmppConnectionService
|
|
||||||
.getString(R.string.unread_conversations));
|
|
||||||
mBuilder.setContentText(names.toString());
|
mBuilder.setContentText(names.toString());
|
||||||
mBuilder.setStyle(style);
|
mBuilder.setStyle(style);
|
||||||
if (conversation != null) {
|
if (conversation != null) {
|
||||||
|
@ -627,8 +618,11 @@ public class NotificationService {
|
||||||
CharSequence text = getMergedBodies(tmp);
|
CharSequence text = getMergedBodies(tmp);
|
||||||
bigPictureStyle.setSummaryText(text);
|
bigPictureStyle.setSummaryText(text);
|
||||||
builder.setContentText(text);
|
builder.setContentText(text);
|
||||||
|
builder.setTicker(text);
|
||||||
} else {
|
} else {
|
||||||
builder.setContentText(UIHelper.getFileDescriptionString(mXmppConnectionService, message));
|
final String description = UIHelper.getFileDescriptionString(mXmppConnectionService, message);
|
||||||
|
builder.setContentText(description);
|
||||||
|
builder.setTicker(description);
|
||||||
}
|
}
|
||||||
builder.setStyle(bigPictureStyle);
|
builder.setStyle(bigPictureStyle);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
|
@ -685,7 +679,9 @@ public class NotificationService {
|
||||||
} else {
|
} else {
|
||||||
if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
|
if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
|
||||||
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
|
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
|
||||||
builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size()-1)).first);
|
final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size()-1)).first;
|
||||||
|
builder.setContentText(preview);
|
||||||
|
builder.setTicker(preview);
|
||||||
builder.setNumber(messages.size());
|
builder.setNumber(messages.size());
|
||||||
} else {
|
} else {
|
||||||
final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
|
final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
|
||||||
|
@ -703,8 +699,11 @@ public class NotificationService {
|
||||||
styledString = new SpannableString(name + ": " + messages.get(0).getBody());
|
styledString = new SpannableString(name + ": " + messages.get(0).getBody());
|
||||||
styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
|
styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0);
|
||||||
builder.setContentText(styledString);
|
builder.setContentText(styledString);
|
||||||
|
builder.setTicker(styledString);
|
||||||
} else {
|
} else {
|
||||||
builder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count));
|
final String text = mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count);
|
||||||
|
builder.setContentText(text);
|
||||||
|
builder.setTicker(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,12 +66,16 @@ public class Resolver {
|
||||||
Result result = new Result();
|
Result result = new Result();
|
||||||
result.hostname = DNSName.from(hostname);
|
result.hostname = DNSName.from(hostname);
|
||||||
result.port = port;
|
result.port = port;
|
||||||
result.directTls = port == 443 || port == 5223;
|
result.directTls = useDirectTls(port);
|
||||||
result.authenticated = true;
|
result.authenticated = true;
|
||||||
return Collections.singletonList(result);
|
return Collections.singletonList(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static boolean useDirectTls(final int port) {
|
||||||
|
return port == 443 || port == 5223;
|
||||||
|
}
|
||||||
|
|
||||||
public static List<Result> resolve(String domain) {
|
public static List<Result> resolve(String domain) {
|
||||||
final List<Result> ipResults = fromIpAddress(domain);
|
final List<Result> ipResults = fromIpAddress(domain);
|
||||||
if (ipResults.size() > 0) {
|
if (ipResults.size() > 0) {
|
||||||
|
|
|
@ -20,6 +20,9 @@ public class SocksSocketFactory {
|
||||||
proxyOs.write(new byte[]{0x05, 0x01, 0x00});
|
proxyOs.write(new byte[]{0x05, 0x01, 0x00});
|
||||||
byte[] response = new byte[2];
|
byte[] response = new byte[2];
|
||||||
proxyIs.read(response);
|
proxyIs.read(response);
|
||||||
|
if (response[0] != 0x05 || response[1] != 0x00) {
|
||||||
|
throw new SocksConnectionException("Socks 5 handshake failed");
|
||||||
|
}
|
||||||
byte[] dest = destination.getBytes();
|
byte[] dest = destination.getBytes();
|
||||||
ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
|
ByteBuffer request = ByteBuffer.allocate(7 + dest.length);
|
||||||
request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
|
request.put(new byte[]{0x05, 0x01, 0x00, 0x03});
|
||||||
|
@ -30,8 +33,23 @@ public class SocksSocketFactory {
|
||||||
response = new byte[7 + dest.length];
|
response = new byte[7 + dest.length];
|
||||||
proxyIs.read(response);
|
proxyIs.read(response);
|
||||||
if (response[1] != 0x00) {
|
if (response[1] != 0x00) {
|
||||||
throw new SocksConnectionException();
|
if (response[1] == 0x04) {
|
||||||
|
throw new HostNotFoundException("Host unreachable");
|
||||||
}
|
}
|
||||||
|
if (response[1] == 0x05) {
|
||||||
|
throw new HostNotFoundException("Connection refused");
|
||||||
|
}
|
||||||
|
throw new SocksConnectionException("Unable to connect to destination "+(int) (response[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean contains(byte needle, byte[] haystack) {
|
||||||
|
for(byte hay : haystack) {
|
||||||
|
if (hay == needle) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
|
public static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException {
|
||||||
|
@ -49,11 +67,19 @@ public class SocksSocketFactory {
|
||||||
return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port);
|
return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
static class SocksConnectionException extends IOException {
|
private static class SocksConnectionException extends IOException {
|
||||||
|
SocksConnectionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SocksProxyNotFoundException extends IOException {
|
public static class SocksProxyNotFoundException extends IOException {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class HostNotFoundException extends SocksConnectionException {
|
||||||
|
HostNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,4 +30,6 @@ public final class Namespace {
|
||||||
public static final String PING = "urn:xmpp:ping";
|
public static final String PING = "urn:xmpp:ping";
|
||||||
public static final String PUSH = "urn:xmpp:push:0";
|
public static final String PUSH = "urn:xmpp:push:0";
|
||||||
public static final String COMMANDS = "http://jabber.org/protocol/commands";
|
public static final String COMMANDS = "http://jabber.org/protocol/commands";
|
||||||
|
public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0";
|
||||||
|
public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0";
|
||||||
}
|
}
|
||||||
|
|
|
@ -267,8 +267,18 @@ public class XmppConnection implements Runnable {
|
||||||
destination = account.getHostname();
|
destination = account.getHostname();
|
||||||
this.verifiedHostname = destination;
|
this.verifiedHostname = destination;
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor");
|
|
||||||
localSocket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
|
final int port = account.getPort();
|
||||||
|
final boolean directTls = Resolver.useDirectTls(port);
|
||||||
|
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor. directTls="+directTls);
|
||||||
|
localSocket = SocksSocketFactory.createSocketOverTor(destination, port);
|
||||||
|
|
||||||
|
if (directTls) {
|
||||||
|
localSocket = upgradeSocketToTls(localSocket);
|
||||||
|
features.encryptionEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startXmpp(localSocket);
|
startXmpp(localSocket);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
@ -310,29 +320,13 @@ public class XmppConnection implements Runnable {
|
||||||
+ ": using values from resolver "
|
+ ": using values from resolver "
|
||||||
+ result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled);
|
+ result.getHostname().toString() + ":" + result.getPort() + " tls: " + features.encryptionEnabled);
|
||||||
|
|
||||||
if (!features.encryptionEnabled) {
|
|
||||||
localSocket = new Socket();
|
localSocket = new Socket();
|
||||||
localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
|
localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
|
||||||
} else {
|
|
||||||
final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
|
|
||||||
localSocket = tlsFactoryVerifier.factory.createSocket();
|
|
||||||
|
|
||||||
if (localSocket == null) {
|
if (features.encryptionEnabled) {
|
||||||
throw new IOException("could not initialize ssl socket");
|
localSocket = upgradeSocketToTls(localSocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
SSLSocketHelper.setSecurity((SSLSocket) localSocket);
|
|
||||||
SSLSocketHelper.setHostname((SSLSocket) localSocket, account.getServer());
|
|
||||||
SSLSocketHelper.setApplicationProtocol((SSLSocket) localSocket, "xmpp-client");
|
|
||||||
|
|
||||||
localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
|
|
||||||
|
|
||||||
if (!tlsFactoryVerifier.verifier.verify(account.getServer(), verifiedHostname, ((SSLSocket) localSocket).getSession())) {
|
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
|
|
||||||
FileBackend.close(localSocket);
|
|
||||||
throw new StateChangingException(Account.State.TLS_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000);
|
localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||||
if (startXmpp(localSocket)) {
|
if (startXmpp(localSocket)) {
|
||||||
localSocket.setSoTimeout(0); //reset to 0; once the connection is established we don’t want this
|
localSocket.setSoTimeout(0); //reset to 0; once the connection is established we don’t want this
|
||||||
|
@ -363,6 +357,8 @@ public class XmppConnection implements Runnable {
|
||||||
this.changeStatus(e.state);
|
this.changeStatus(e.state);
|
||||||
} catch (final UnknownHostException | ConnectException e) {
|
} catch (final UnknownHostException | ConnectException e) {
|
||||||
this.changeStatus(Account.State.SERVER_NOT_FOUND);
|
this.changeStatus(Account.State.SERVER_NOT_FOUND);
|
||||||
|
} catch (final SocksSocketFactory.HostNotFoundException e) {
|
||||||
|
this.changeStatus(Account.State.SERVER_NOT_FOUND);
|
||||||
} catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
|
} catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
|
||||||
this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
|
this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
|
||||||
} catch (final IOException | XmlPullParserException e) {
|
} catch (final IOException | XmlPullParserException e) {
|
||||||
|
@ -775,29 +771,8 @@ public class XmppConnection implements Runnable {
|
||||||
|
|
||||||
private void switchOverToTls() throws XmlPullParserException, IOException {
|
private void switchOverToTls() throws XmlPullParserException, IOException {
|
||||||
tagReader.readTag();
|
tagReader.readTag();
|
||||||
try {
|
final Socket socket = this.socket;
|
||||||
final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
|
final SSLSocket sslSocket = upgradeSocketToTls(socket);
|
||||||
final InetAddress address = socket == null ? null : socket.getInetAddress();
|
|
||||||
|
|
||||||
if (address == null) {
|
|
||||||
throw new IOException("could not setup ssl");
|
|
||||||
}
|
|
||||||
|
|
||||||
final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
|
|
||||||
|
|
||||||
|
|
||||||
if (sslSocket == null) {
|
|
||||||
throw new IOException("could not initialize ssl socket");
|
|
||||||
}
|
|
||||||
|
|
||||||
SSLSocketHelper.setSecurity(sslSocket);
|
|
||||||
SSLSocketHelper.setHostname(sslSocket, account.getServer());
|
|
||||||
SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client");
|
|
||||||
|
|
||||||
if (!tlsFactoryVerifier.verifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
|
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
|
|
||||||
throw new StateChangingException(Account.State.TLS_ERROR);
|
|
||||||
}
|
|
||||||
tagReader.setInputStream(sslSocket.getInputStream());
|
tagReader.setInputStream(sslSocket.getInputStream());
|
||||||
tagWriter.setOutputStream(sslSocket.getOutputStream());
|
tagWriter.setOutputStream(sslSocket.getOutputStream());
|
||||||
sendStartStream();
|
sendStartStream();
|
||||||
|
@ -811,10 +786,26 @@ public class XmppConnection implements Runnable {
|
||||||
throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
|
throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
|
||||||
}
|
}
|
||||||
sslSocket.close();
|
sslSocket.close();
|
||||||
} catch (final NoSuchAlgorithmException | KeyManagementException e1) {
|
}
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
|
|
||||||
|
private SSLSocket upgradeSocketToTls(final Socket socket) throws IOException {
|
||||||
|
final TlsFactoryVerifier tlsFactoryVerifier;
|
||||||
|
try {
|
||||||
|
tlsFactoryVerifier = getTlsFactoryVerifier();
|
||||||
|
} catch (final NoSuchAlgorithmException | KeyManagementException e) {
|
||||||
throw new StateChangingException(Account.State.TLS_ERROR);
|
throw new StateChangingException(Account.State.TLS_ERROR);
|
||||||
}
|
}
|
||||||
|
final InetAddress address = socket.getInetAddress();
|
||||||
|
final SSLSocket sslSocket = (SSLSocket) tlsFactoryVerifier.factory.createSocket(socket, address.getHostAddress(), socket.getPort(), true);
|
||||||
|
SSLSocketHelper.setSecurity(sslSocket);
|
||||||
|
SSLSocketHelper.setHostname(sslSocket, account.getServer());
|
||||||
|
SSLSocketHelper.setApplicationProtocol(sslSocket, "xmpp-client");
|
||||||
|
if (!tlsFactoryVerifier.verifier.verify(account.getServer(), this.verifiedHostname, sslSocket.getSession())) {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS certificate verification failed");
|
||||||
|
FileBackend.close(sslSocket);
|
||||||
|
throw new StateChangingException(Account.State.TLS_ERROR);
|
||||||
|
}
|
||||||
|
return sslSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processStreamFeatures(final Tag currentTag) throws XmlPullParserException, IOException {
|
private void processStreamFeatures(final Tag currentTag) throws XmlPullParserException, IOException {
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import java.net.Inet6Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import rocks.xmpp.addr.Jid;
|
||||||
|
|
||||||
|
public class DirectConnectionUtils {
|
||||||
|
|
||||||
|
private static List<InetAddress> getLocalAddresses() {
|
||||||
|
final List<InetAddress> addresses = new ArrayList<>();
|
||||||
|
final Enumeration<NetworkInterface> interfaces;
|
||||||
|
try {
|
||||||
|
interfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
|
} catch (SocketException e) {
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
while (interfaces.hasMoreElements()) {
|
||||||
|
NetworkInterface networkInterface = interfaces.nextElement();
|
||||||
|
final Enumeration<InetAddress> inetAddressEnumeration = networkInterface.getInetAddresses();
|
||||||
|
while (inetAddressEnumeration.hasMoreElements()) {
|
||||||
|
final InetAddress inetAddress = inetAddressEnumeration.nextElement();
|
||||||
|
if (inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inetAddress instanceof Inet6Address) {
|
||||||
|
//let's get rid of scope
|
||||||
|
try {
|
||||||
|
addresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
//ignored
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addresses.add(inetAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<JingleCandidate> getLocalCandidates(Jid jid) {
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
ArrayList<JingleCandidate> candidates = new ArrayList<>();
|
||||||
|
for (InetAddress inetAddress : getLocalAddresses()) {
|
||||||
|
final JingleCandidate candidate = new JingleCandidate(UUID.randomUUID().toString(), true);
|
||||||
|
candidate.setHost(inetAddress.getHostAddress());
|
||||||
|
candidate.setPort(random.nextInt(60000) + 1024);
|
||||||
|
candidate.setType(JingleCandidate.TYPE_DIRECT);
|
||||||
|
candidate.setJid(jid);
|
||||||
|
candidate.setPriority(8257536 + candidates.size());
|
||||||
|
candidates.add(candidate);
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -110,15 +110,12 @@ public class JingleCandidate {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JingleCandidate parse(Element candidate) {
|
public static JingleCandidate parse(Element candidate) {
|
||||||
JingleCandidate parsedCandidate = new JingleCandidate(
|
JingleCandidate parsedCandidate = new JingleCandidate(candidate.getAttribute("cid"), false);
|
||||||
candidate.getAttribute("cid"), false);
|
|
||||||
parsedCandidate.setHost(candidate.getAttribute("host"));
|
parsedCandidate.setHost(candidate.getAttribute("host"));
|
||||||
parsedCandidate.setJid(InvalidJid.getNullForInvalid(candidate.getAttributeAsJid("jid")));
|
parsedCandidate.setJid(InvalidJid.getNullForInvalid(candidate.getAttributeAsJid("jid")));
|
||||||
parsedCandidate.setType(candidate.getAttribute("type"));
|
parsedCandidate.setType(candidate.getAttribute("type"));
|
||||||
parsedCandidate.setPriority(Integer.parseInt(candidate
|
parsedCandidate.setPriority(Integer.parseInt(candidate.getAttribute("priority")));
|
||||||
.getAttribute("priority")));
|
parsedCandidate.setPort(Integer.parseInt(candidate.getAttribute("port")));
|
||||||
parsedCandidate
|
|
||||||
.setPort(Integer.parseInt(candidate.getAttribute("port")));
|
|
||||||
return parsedCandidate;
|
return parsedCandidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +124,9 @@ public class JingleCandidate {
|
||||||
element.setAttribute("cid", this.getCid());
|
element.setAttribute("cid", this.getCid());
|
||||||
element.setAttribute("host", this.getHost());
|
element.setAttribute("host", this.getHost());
|
||||||
element.setAttribute("port", Integer.toString(this.getPort()));
|
element.setAttribute("port", Integer.toString(this.getPort()));
|
||||||
element.setAttribute("jid", this.getJid().toString());
|
if (jid != null) {
|
||||||
|
element.setAttribute("jid", jid.toEscapedString());
|
||||||
|
}
|
||||||
element.setAttribute("priority", Integer.toString(this.getPriority()));
|
element.setAttribute("priority", Integer.toString(this.getPriority()));
|
||||||
if (this.getType() == TYPE_DIRECT) {
|
if (this.getType() == TYPE_DIRECT) {
|
||||||
element.setAttribute("type", "direct");
|
element.setAttribute("type", "direct");
|
||||||
|
|
|
@ -43,6 +43,8 @@ import rocks.xmpp.addr.Jid;
|
||||||
|
|
||||||
public class JingleConnection implements Transferable {
|
public class JingleConnection implements Transferable {
|
||||||
|
|
||||||
|
private static final String JET_OMEMO_CIPHER = "urn:xmpp:ciphers:aes-128-gcm-nopadding";
|
||||||
|
|
||||||
private static final int JINGLE_STATUS_INITIATED = 0;
|
private static final int JINGLE_STATUS_INITIATED = 0;
|
||||||
private static final int JINGLE_STATUS_ACCEPTED = 1;
|
private static final int JINGLE_STATUS_ACCEPTED = 1;
|
||||||
private static final int JINGLE_STATUS_FINISHED = 4;
|
private static final int JINGLE_STATUS_FINISHED = 4;
|
||||||
|
@ -72,6 +74,7 @@ public class JingleConnection implements Transferable {
|
||||||
private String contentName;
|
private String contentName;
|
||||||
private String contentCreator;
|
private String contentCreator;
|
||||||
private Transport initialTransport;
|
private Transport initialTransport;
|
||||||
|
private boolean remoteSupportsOmemoJet;
|
||||||
|
|
||||||
private int mProgress = 0;
|
private int mProgress = 0;
|
||||||
|
|
||||||
|
@ -171,7 +174,10 @@ public class JingleConnection implements Transferable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void failed() {
|
public void failed() {
|
||||||
Log.d(Config.LOGTAG, "proxy activation failed");
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": proxy activation failed");
|
||||||
|
if (initiating()) {
|
||||||
|
sendFallbackToIbb();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -292,8 +298,10 @@ public class JingleConnection implements Transferable {
|
||||||
this.contentName = this.mJingleConnectionManager.nextRandomId();
|
this.contentName = this.mJingleConnectionManager.nextRandomId();
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.account = message.getConversation().getAccount();
|
this.account = message.getConversation().getAccount();
|
||||||
upgradeNamespace();
|
final List<String> remoteFeatures = getRemoteFeatures();
|
||||||
this.initialTransport = getRemoteFeatures().contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB;
|
upgradeNamespace(remoteFeatures);
|
||||||
|
this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB;
|
||||||
|
this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO);
|
||||||
this.message.setTransferable(this);
|
this.message.setTransferable(this);
|
||||||
this.mStatus = Transferable.STATUS_UPLOADING;
|
this.mStatus = Transferable.STATUS_UPLOADING;
|
||||||
this.initiator = this.account.getJid();
|
this.initiator = this.account.getJid();
|
||||||
|
@ -302,10 +310,9 @@ public class JingleConnection implements Transferable {
|
||||||
this.transportId = this.mJingleConnectionManager.nextRandomId();
|
this.transportId = this.mJingleConnectionManager.nextRandomId();
|
||||||
if (this.initialTransport == Transport.IBB) {
|
if (this.initialTransport == Transport.IBB) {
|
||||||
this.sendInitRequest();
|
this.sendInitRequest();
|
||||||
} else if (this.candidates.size() > 0) {
|
|
||||||
this.sendInitRequest();
|
|
||||||
} else {
|
} else {
|
||||||
this.mJingleConnectionManager.getPrimaryCandidate(account, (success, candidate) -> {
|
gatherAndConnectDirectCandidates();
|
||||||
|
this.mJingleConnectionManager.getPrimaryCandidate(account, initiating(), (success, candidate) -> {
|
||||||
if (success) {
|
if (success) {
|
||||||
final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
|
final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
|
||||||
connections.put(candidate.getCid(), socksConnection);
|
connections.put(candidate.getCid(), socksConnection);
|
||||||
|
@ -313,22 +320,20 @@ public class JingleConnection implements Transferable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void failed() {
|
public void failed() {
|
||||||
Log.d(Config.LOGTAG,
|
Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed");
|
||||||
"connection to our own primary candidete failed");
|
|
||||||
sendInitRequest();
|
sendInitRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void established() {
|
public void established() {
|
||||||
Log.d(Config.LOGTAG,
|
Log.d(Config.LOGTAG, "successfully connected to our own proxy65 candidate");
|
||||||
"successfully connected to our own primary candidate");
|
|
||||||
mergeCandidate(candidate);
|
mergeCandidate(candidate);
|
||||||
sendInitRequest();
|
sendInitRequest();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mergeCandidate(candidate);
|
mergeCandidate(candidate);
|
||||||
} else {
|
} else {
|
||||||
Log.d(Config.LOGTAG, "no primary candidate of our own was found");
|
Log.d(Config.LOGTAG, "no proxy65 candidate of our own was found");
|
||||||
sendInitRequest();
|
sendInitRequest();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -336,11 +341,28 @@ public class JingleConnection implements Transferable {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upgradeNamespace() {
|
private void gatherAndConnectDirectCandidates() {
|
||||||
List<String> features = getRemoteFeatures();
|
final List<JingleCandidate> directCandidates;
|
||||||
if (features.contains(Content.Version.FT_5.getNamespace())) {
|
if (Config.USE_DIRECT_JINGLE_CANDIDATES) {
|
||||||
|
if (account.isOnion() || mXmppConnectionService.useTorToConnect()) {
|
||||||
|
directCandidates = Collections.emptyList();
|
||||||
|
} else {
|
||||||
|
directCandidates = DirectConnectionUtils.getLocalCandidates(account.getJid());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
directCandidates = Collections.emptyList();
|
||||||
|
}
|
||||||
|
for (JingleCandidate directCandidate : directCandidates) {
|
||||||
|
final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, directCandidate);
|
||||||
|
connections.put(directCandidate.getCid(), socksConnection);
|
||||||
|
candidates.add(directCandidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upgradeNamespace(List<String> remoteFeatures) {
|
||||||
|
if (remoteFeatures.contains(Content.Version.FT_5.getNamespace())) {
|
||||||
this.ftVersion = Content.Version.FT_5;
|
this.ftVersion = Content.Version.FT_5;
|
||||||
} else if (features.contains(Content.Version.FT_4.getNamespace())) {
|
} else if (remoteFeatures.contains(Content.Version.FT_4.getNamespace())) {
|
||||||
this.ftVersion = Content.Version.FT_4;
|
this.ftVersion = Content.Version.FT_4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -408,8 +430,18 @@ public class JingleConnection implements Transferable {
|
||||||
}
|
}
|
||||||
this.fileOffer = content.getFileOffer(this.ftVersion);
|
this.fileOffer = content.getFileOffer(this.ftVersion);
|
||||||
|
|
||||||
|
|
||||||
if (fileOffer != null) {
|
if (fileOffer != null) {
|
||||||
|
boolean remoteIsUsingJet = false;
|
||||||
Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
|
Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
|
||||||
|
if (encrypted == null) {
|
||||||
|
final Element security = content.findChild("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
|
||||||
|
if (security != null && AxolotlService.PEP_PREFIX.equals(security.getAttribute("type"))) {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received jingle file offer with JET");
|
||||||
|
encrypted = security.findChild("encrypted", AxolotlService.PEP_PREFIX);
|
||||||
|
remoteIsUsingJet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (encrypted != null) {
|
if (encrypted != null) {
|
||||||
this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid());
|
this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid());
|
||||||
}
|
}
|
||||||
|
@ -450,7 +482,10 @@ public class JingleConnection implements Transferable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message.resetFileParams();
|
message.resetFileParams();
|
||||||
this.file.setExpectedSize(size);
|
//legacy OMEMO encrypted file transfers reported the file size after encryption
|
||||||
|
//JET reports the plain text size. however lower levels of our receiving code still
|
||||||
|
//expect the cipher text size. so we just + 16 bytes (auth tag size) here
|
||||||
|
this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0));
|
||||||
if (mJingleConnectionManager.hasStoragePermission()
|
if (mJingleConnectionManager.hasStoragePermission()
|
||||||
&& size < this.mJingleConnectionManager.getAutoAcceptFileSize()
|
&& size < this.mJingleConnectionManager.getAutoAcceptFileSize()
|
||||||
&& mXmppConnectionService.isDataSaverDisabled()) {
|
&& mXmppConnectionService.isDataSaverDisabled()) {
|
||||||
|
@ -499,8 +534,21 @@ public class JingleConnection implements Transferable {
|
||||||
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
|
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
|
||||||
this.file.setKey(mXmppAxolotlMessage.getInnerKey());
|
this.file.setKey(mXmppAxolotlMessage.getInnerKey());
|
||||||
this.file.setIv(mXmppAxolotlMessage.getIV());
|
this.file.setIv(mXmppAxolotlMessage.getIV());
|
||||||
this.file.setExpectedSize(file.getSize() + 16);
|
//legacy OMEMO encrypted file transfer reported file size of the encrypted file
|
||||||
content.setFileOffer(this.file, false, this.ftVersion).addChild(mXmppAxolotlMessage.toElement());
|
//JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag)
|
||||||
|
this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16));
|
||||||
|
final Element file = content.setFileOffer(this.file, false, this.ftVersion);
|
||||||
|
if (remoteSupportsOmemoJet) {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote announced support for JET");
|
||||||
|
final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
|
||||||
|
security.setAttribute("name", this.contentName);
|
||||||
|
security.setAttribute("cipher", JET_OMEMO_CIPHER);
|
||||||
|
security.setAttribute("type", AxolotlService.PEP_PREFIX);
|
||||||
|
security.addChild(mXmppAxolotlMessage.toElement());
|
||||||
|
content.addChild(security);
|
||||||
|
} else {
|
||||||
|
file.addChild(mXmppAxolotlMessage.toElement());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.file.setExpectedSize(file.getSize());
|
this.file.setExpectedSize(file.getSize());
|
||||||
content.setFileOffer(this.file, false, this.ftVersion);
|
content.setFileOffer(this.file, false, this.ftVersion);
|
||||||
|
@ -564,7 +612,8 @@ public class JingleConnection implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendAcceptSocks() {
|
private void sendAcceptSocks() {
|
||||||
this.mJingleConnectionManager.getPrimaryCandidate(this.account, (success, candidate) -> {
|
gatherAndConnectDirectCandidates();
|
||||||
|
this.mJingleConnectionManager.getPrimaryCandidate(this.account, initiating(), (success, candidate) -> {
|
||||||
final JinglePacket packet = bootstrapPacket("session-accept");
|
final JinglePacket packet = bootstrapPacket("session-accept");
|
||||||
final Content content = new Content(contentCreator, contentName);
|
final Content content = new Content(contentCreator, contentName);
|
||||||
content.setFileOffer(fileOffer, ftVersion);
|
content.setFileOffer(fileOffer, ftVersion);
|
||||||
|
@ -576,7 +625,7 @@ public class JingleConnection implements Transferable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void failed() {
|
public void failed() {
|
||||||
Log.d(Config.LOGTAG, "connection to our own primary candidate failed");
|
Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed");
|
||||||
content.socks5transport().setChildren(getCandidatesAsElements());
|
content.socks5transport().setChildren(getCandidatesAsElements());
|
||||||
packet.setContent(content);
|
packet.setContent(content);
|
||||||
sendJinglePacket(packet);
|
sendJinglePacket(packet);
|
||||||
|
@ -585,7 +634,7 @@ public class JingleConnection implements Transferable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void established() {
|
public void established() {
|
||||||
Log.d(Config.LOGTAG, "connected to primary candidate");
|
Log.d(Config.LOGTAG, "connected to proxy65 candidate");
|
||||||
mergeCandidate(candidate);
|
mergeCandidate(candidate);
|
||||||
content.socks5transport().setChildren(getCandidatesAsElements());
|
content.socks5transport().setChildren(getCandidatesAsElements());
|
||||||
packet.setContent(content);
|
packet.setContent(content);
|
||||||
|
@ -594,7 +643,7 @@ public class JingleConnection implements Transferable {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Log.d(Config.LOGTAG, "did not find a primary candidate for ourself");
|
Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves");
|
||||||
content.socks5transport().setChildren(getCandidatesAsElements());
|
content.socks5transport().setChildren(getCandidatesAsElements());
|
||||||
packet.setContent(content);
|
packet.setContent(content);
|
||||||
sendJinglePacket(packet);
|
sendJinglePacket(packet);
|
||||||
|
@ -689,7 +738,7 @@ public class JingleConnection implements Transferable {
|
||||||
onProxyActivated.failed();
|
onProxyActivated.failed();
|
||||||
return true;
|
return true;
|
||||||
} else if (content.socks5transport().hasChild("candidate-error")) {
|
} else if (content.socks5transport().hasChild("candidate-error")) {
|
||||||
Log.d(Config.LOGTAG, "received candidate error");
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received candidate error");
|
||||||
this.receivedCandidate = true;
|
this.receivedCandidate = true;
|
||||||
if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
|
if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
|
||||||
this.connect();
|
this.connect();
|
||||||
|
@ -727,12 +776,14 @@ public class JingleConnection implements Transferable {
|
||||||
final JingleSocks5Transport connection = chooseConnection();
|
final JingleSocks5Transport connection = chooseConnection();
|
||||||
this.transport = connection;
|
this.transport = connection;
|
||||||
if (connection == null) {
|
if (connection == null) {
|
||||||
Log.d(Config.LOGTAG, "could not find suitable candidate");
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not find suitable candidate");
|
||||||
this.disconnectSocks5Connections();
|
this.disconnectSocks5Connections();
|
||||||
if (initiating()) {
|
if (initiating()) {
|
||||||
this.sendFallbackToIbb();
|
this.sendFallbackToIbb();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
final JingleCandidate candidate = connection.getCandidate();
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": elected candidate " + candidate.getHost() + ":" + candidate.getPort());
|
||||||
this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
|
this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
|
||||||
if (connection.needsActivation()) {
|
if (connection.needsActivation()) {
|
||||||
if (connection.getCandidate().isOurs()) {
|
if (connection.getCandidate().isOurs()) {
|
||||||
|
@ -754,10 +805,12 @@ public class JingleConnection implements Transferable {
|
||||||
.setContent(this.getCounterPart().toString());
|
.setContent(this.getCounterPart().toString());
|
||||||
mXmppConnectionService.sendIqPacket(account, activation, (account, response) -> {
|
mXmppConnectionService.sendIqPacket(account, activation, (account, response) -> {
|
||||||
if (response.getType() != IqPacket.TYPE.RESULT) {
|
if (response.getType() != IqPacket.TYPE.RESULT) {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + response.toString());
|
||||||
|
sendProxyError();
|
||||||
onProxyActivated.failed();
|
onProxyActivated.failed();
|
||||||
} else {
|
} else {
|
||||||
onProxyActivated.success();
|
|
||||||
sendProxyActivated(connection.getCandidate().getCid());
|
sendProxyActivated(connection.getCandidate().getCid());
|
||||||
|
onProxyActivated.success();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -841,7 +894,7 @@ public class JingleConnection implements Transferable {
|
||||||
|
|
||||||
|
|
||||||
private boolean receiveFallbackToIbb(JinglePacket packet) {
|
private boolean receiveFallbackToIbb(JinglePacket packet) {
|
||||||
Log.d(Config.LOGTAG, "receiving fallback to ibb");
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receiving fallback to ibb");
|
||||||
final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
|
final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size");
|
||||||
if (receivedBlockSize != null) {
|
if (receivedBlockSize != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -1029,14 +1082,23 @@ public class JingleConnection implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendProxyActivated(String cid) {
|
private void sendProxyActivated(String cid) {
|
||||||
JinglePacket packet = bootstrapPacket("transport-info");
|
final JinglePacket packet = bootstrapPacket("transport-info");
|
||||||
Content content = new Content(this.contentCreator, this.contentName);
|
final Content content = new Content(this.contentCreator, this.contentName);
|
||||||
content.setTransportId(this.transportId);
|
content.setTransportId(this.transportId);
|
||||||
content.socks5transport().addChild("activated").setAttribute("cid", cid);
|
content.socks5transport().addChild("activated").setAttribute("cid", cid);
|
||||||
packet.setContent(content);
|
packet.setContent(content);
|
||||||
this.sendJinglePacket(packet);
|
this.sendJinglePacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sendProxyError() {
|
||||||
|
final JinglePacket packet = bootstrapPacket("transport-info");
|
||||||
|
final Content content = new Content(this.contentCreator, this.contentName);
|
||||||
|
content.setTransportId(this.transportId);
|
||||||
|
content.socks5transport().addChild("proxy-error");
|
||||||
|
packet.setContent(content);
|
||||||
|
this.sendJinglePacket(packet);
|
||||||
|
}
|
||||||
|
|
||||||
private void sendCandidateUsed(final String cid) {
|
private void sendCandidateUsed(final String cid) {
|
||||||
JinglePacket packet = bootstrapPacket("transport-info");
|
JinglePacket packet = bootstrapPacket("transport-info");
|
||||||
Content content = new Content(this.contentCreator, this.contentName);
|
Content content = new Content(this.contentCreator, this.contentName);
|
||||||
|
@ -1051,7 +1113,7 @@ public class JingleConnection implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendCandidateError() {
|
private void sendCandidateError() {
|
||||||
Log.d(Config.LOGTAG, "sending candidate error");
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending candidate error");
|
||||||
JinglePacket packet = bootstrapPacket("transport-info");
|
JinglePacket packet = bootstrapPacket("transport-info");
|
||||||
Content content = new Content(this.contentCreator, this.contentName);
|
Content content = new Content(this.contentCreator, this.contentName);
|
||||||
content.setTransportId(this.transportId);
|
content.setTransportId(this.transportId);
|
||||||
|
@ -1087,6 +1149,7 @@ public class JingleConnection implements Transferable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mergeCandidates(List<JingleCandidate> candidates) {
|
private void mergeCandidates(List<JingleCandidate> candidates) {
|
||||||
|
Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
|
||||||
for (JingleCandidate c : candidates) {
|
for (JingleCandidate c : candidates) {
|
||||||
mergeCandidate(c);
|
mergeCandidate(c);
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,8 +81,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
this.connections.remove(connection);
|
this.connections.remove(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getPrimaryCandidate(Account account,
|
public void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) {
|
||||||
final OnPrimaryCandidateFound listener) {
|
|
||||||
if (Config.DISABLE_PROXY_LOOKUP) {
|
if (Config.DISABLE_PROXY_LOOKUP) {
|
||||||
listener.onPrimaryCandidateFound(false, null);
|
listener.onPrimaryCandidateFound(false, null);
|
||||||
return;
|
return;
|
||||||
|
@ -107,7 +106,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
candidate.setPort(Integer.parseInt(port));
|
candidate.setPort(Integer.parseInt(port));
|
||||||
candidate.setType(JingleCandidate.TYPE_PROXY);
|
candidate.setType(JingleCandidate.TYPE_PROXY);
|
||||||
candidate.setJid(proxy);
|
candidate.setJid(proxy);
|
||||||
candidate.setPriority(655360 + 65535);
|
candidate.setPriority(655360 + (initiator ? 10 : 20));
|
||||||
primaryCandidates.put(account.getJid().asBareJid(),candidate);
|
primaryCandidates.put(account.getJid().asBareJid(),candidate);
|
||||||
listener.onPrimaryCandidateFound(true,candidate);
|
listener.onPrimaryCandidateFound(true,candidate);
|
||||||
} catch (final NumberFormatException e) {
|
} catch (final NumberFormatException e) {
|
||||||
|
|
|
@ -6,13 +6,17 @@ import android.util.Log;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.DownloadableFile;
|
import eu.siacs.conversations.entities.DownloadableFile;
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||||
|
@ -22,21 +26,26 @@ import eu.siacs.conversations.utils.WakeLockHelper;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||||
|
|
||||||
public class JingleSocks5Transport extends JingleTransport {
|
public class JingleSocks5Transport extends JingleTransport {
|
||||||
private JingleCandidate candidate;
|
private final JingleCandidate candidate;
|
||||||
private JingleConnection connection;
|
private final JingleConnection connection;
|
||||||
private String destination;
|
private final String destination;
|
||||||
private OutputStream outputStream;
|
private OutputStream outputStream;
|
||||||
private InputStream inputStream;
|
private InputStream inputStream;
|
||||||
private boolean isEstablished = false;
|
private boolean isEstablished = false;
|
||||||
private boolean activated = false;
|
private boolean activated = false;
|
||||||
|
private ServerSocket serverSocket;
|
||||||
private Socket socket;
|
private Socket socket;
|
||||||
|
|
||||||
JingleSocks5Transport(JingleConnection jingleConnection, JingleCandidate candidate) {
|
JingleSocks5Transport(JingleConnection jingleConnection, JingleCandidate candidate) {
|
||||||
|
final MessageDigest messageDigest;
|
||||||
|
try {
|
||||||
|
messageDigest = MessageDigest.getInstance("SHA-1");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
this.candidate = candidate;
|
this.candidate = candidate;
|
||||||
this.connection = jingleConnection;
|
this.connection = jingleConnection;
|
||||||
try {
|
final StringBuilder destBuilder = new StringBuilder();
|
||||||
MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
|
|
||||||
StringBuilder destBuilder = new StringBuilder();
|
|
||||||
if (jingleConnection.getFtVersion() == Content.Version.FT_3) {
|
if (jingleConnection.getFtVersion() == Content.Version.FT_3) {
|
||||||
Log.d(Config.LOGTAG, this.connection.getAccount().getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination");
|
Log.d(Config.LOGTAG, this.connection.getAccount().getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination");
|
||||||
destBuilder.append(jingleConnection.getSessionId());
|
destBuilder.append(jingleConnection.getSessionId());
|
||||||
|
@ -50,11 +59,93 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
destBuilder.append(jingleConnection.getCounterPart());
|
destBuilder.append(jingleConnection.getCounterPart());
|
||||||
destBuilder.append(jingleConnection.getAccount().getJid());
|
destBuilder.append(jingleConnection.getAccount().getJid());
|
||||||
}
|
}
|
||||||
mDigest.reset();
|
messageDigest.reset();
|
||||||
this.destination = CryptoHelper.bytesToHex(mDigest
|
this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes()));
|
||||||
.digest(destBuilder.toString().getBytes()));
|
if (candidate.isOurs() && candidate.getType() == JingleCandidate.TYPE_DIRECT) {
|
||||||
} catch (NoSuchAlgorithmException e) {
|
createServerSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createServerSocket() {
|
||||||
|
try {
|
||||||
|
serverSocket = new ServerSocket();
|
||||||
|
serverSocket.bind(new InetSocketAddress(InetAddress.getByName(candidate.getHost()), candidate.getPort()));
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
final Socket socket = serverSocket.accept();
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
acceptIncomingSocketConnection(socket);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(Config.LOGTAG, "unable to read from socket", e);
|
||||||
|
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (!serverSocket.isClosed()) {
|
||||||
|
Log.d(Config.LOGTAG, "unable to accept socket", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(Config.LOGTAG, "unable to bind server socket ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptIncomingSocketConnection(Socket socket) throws IOException {
|
||||||
|
Log.d(Config.LOGTAG, "accepted connection from " + socket.getInetAddress().getHostAddress());
|
||||||
|
final byte[] authBegin = new byte[2];
|
||||||
|
final InputStream inputStream = socket.getInputStream();
|
||||||
|
final OutputStream outputStream = socket.getOutputStream();
|
||||||
|
inputStream.read(authBegin);
|
||||||
|
if (authBegin[0] != 0x5) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
final short methodCount = authBegin[1];
|
||||||
|
final byte[] methods = new byte[methodCount];
|
||||||
|
inputStream.read(methods);
|
||||||
|
if (SocksSocketFactory.contains((byte) 0x00, methods)) {
|
||||||
|
outputStream.write(new byte[]{0x05, 0x00});
|
||||||
|
} else {
|
||||||
|
outputStream.write(new byte[]{0x05, (byte) 0xff});
|
||||||
|
}
|
||||||
|
byte[] connectCommand = new byte[4];
|
||||||
|
inputStream.read(connectCommand);
|
||||||
|
if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) {
|
||||||
|
int destinationCount = inputStream.read();
|
||||||
|
final byte[] destination = new byte[destinationCount];
|
||||||
|
inputStream.read(destination);
|
||||||
|
final int port = inputStream.read();
|
||||||
|
final String receivedDestination = new String(destination);
|
||||||
|
final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
|
||||||
|
final byte[] responseHeader;
|
||||||
|
final boolean success;
|
||||||
|
if (receivedDestination.equals(this.destination) && this.socket == null) {
|
||||||
|
responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03};
|
||||||
|
success = true;
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG,connection.getAccount().getJid().asBareJid()+": destination mismatch. received "+receivedDestination+" (expected "+this.destination+")");
|
||||||
|
responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03};
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
response.put(responseHeader);
|
||||||
|
response.put((byte) destination.length);
|
||||||
|
response.put(destination);
|
||||||
|
response.putShort((short) port);
|
||||||
|
outputStream.write(response.array());
|
||||||
|
outputStream.flush();
|
||||||
|
if (success) {
|
||||||
|
Log.d(Config.LOGTAG,connection.getAccount().getJid().asBareJid()+": successfully processed connection to candidate "+candidate.getHost()+":"+candidate.getPort());
|
||||||
|
this.socket = socket;
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
this.outputStream = outputStream;
|
||||||
|
this.isEstablished = true;
|
||||||
|
FileBackend.close(serverSocket);
|
||||||
|
} else {
|
||||||
|
this.socket.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
socket.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,11 +158,13 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
} else {
|
} else {
|
||||||
socket = new Socket();
|
socket = new Socket();
|
||||||
SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort());
|
SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort());
|
||||||
socket.connect(address, Config.SOCKET_TIMEOUT * 1000);
|
socket.connect(address, 5000);
|
||||||
}
|
}
|
||||||
inputStream = socket.getInputStream();
|
inputStream = socket.getInputStream();
|
||||||
outputStream = socket.getOutputStream();
|
outputStream = socket.getOutputStream();
|
||||||
|
socket.setSoTimeout(5000);
|
||||||
SocksSocketFactory.createSocksConnection(socket, destination, 0);
|
SocksSocketFactory.createSocksConnection(socket, destination, 0);
|
||||||
|
socket.setSoTimeout(0);
|
||||||
isEstablished = true;
|
isEstablished = true;
|
||||||
callback.established();
|
callback.established();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -85,6 +178,7 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
InputStream fileInputStream = null;
|
InputStream fileInputStream = null;
|
||||||
final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getSessionId());
|
final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getSessionId());
|
||||||
|
long transmitted = 0;
|
||||||
try {
|
try {
|
||||||
wakeLock.acquire();
|
wakeLock.acquire();
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||||
|
@ -97,7 +191,6 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
}
|
}
|
||||||
final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
|
final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
|
||||||
long size = file.getExpectedSize();
|
long size = file.getExpectedSize();
|
||||||
long transmitted = 0;
|
|
||||||
int count;
|
int count;
|
||||||
byte[] buffer = new byte[8192];
|
byte[] buffer = new byte[8192];
|
||||||
while ((count = innerInputStream.read(buffer)) > 0) {
|
while ((count = innerInputStream.read(buffer)) > 0) {
|
||||||
|
@ -112,7 +205,8 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
callback.onFileTransmitted(file);
|
callback.onFileTransmitted(file);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": " + e.getMessage());
|
final Account account = connection.getAccount();
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": failed sending file after "+transmitted+"/"+file.getExpectedSize()+" ("+ socket.getInetAddress()+":"+socket.getPort()+")", e);
|
||||||
callback.onFileTransferAborted();
|
callback.onFileTransferAborted();
|
||||||
} finally {
|
} finally {
|
||||||
FileBackend.close(fileInputStream);
|
FileBackend.close(fileInputStream);
|
||||||
|
@ -182,6 +276,7 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
FileBackend.close(inputStream);
|
FileBackend.close(inputStream);
|
||||||
FileBackend.close(outputStream);
|
FileBackend.close(outputStream);
|
||||||
FileBackend.close(socket);
|
FileBackend.close(socket);
|
||||||
|
FileBackend.close(serverSocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEstablished() {
|
public boolean isEstablished() {
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
<string name="just_now">الآن</string>
|
<string name="just_now">الآن</string>
|
||||||
<string name="minute_ago">منذ 1 دقيقة</string>
|
<string name="minute_ago">منذ 1 دقيقة</string>
|
||||||
<string name="minutes_ago">دقائق %d منذ</string>
|
<string name="minutes_ago">دقائق %d منذ</string>
|
||||||
<string name="unread_conversations">محادثات غير مقروءة</string>
|
|
||||||
<string name="sending">ارسال</string>
|
<string name="sending">ارسال</string>
|
||||||
<string name="message_decrypting">حل شيفرة الرسالة. الرجاء الإنتظار ...</string>
|
<string name="message_decrypting">حل شيفرة الرسالة. الرجاء الإنتظار ...</string>
|
||||||
<string name="pgp_message">رسالة مشفرة عبر OpenPGP</string>
|
<string name="pgp_message">رسالة مشفرة عبر OpenPGP</string>
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
<string name="just_now">току-що</string>
|
<string name="just_now">току-що</string>
|
||||||
<string name="minute_ago">преди 1 минута</string>
|
<string name="minute_ago">преди 1 минута</string>
|
||||||
<string name="minutes_ago">преди %d минути</string>
|
<string name="minutes_ago">преди %d минути</string>
|
||||||
<string name="unread_conversations">непрочетени разговори</string>
|
|
||||||
<string name="sending">изпращане…</string>
|
<string name="sending">изпращане…</string>
|
||||||
<string name="message_decrypting">Дешифроване на съобщението. Моля, изчакайте…</string>
|
<string name="message_decrypting">Дешифроване на съобщението. Моля, изчакайте…</string>
|
||||||
<string name="pgp_message">Съобщение, шифр. чрез OpenPGP</string>
|
<string name="pgp_message">Съобщение, шифр. чрез OpenPGP</string>
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
<string name="just_now">Ara</string>
|
<string name="just_now">Ara</string>
|
||||||
<string name="minute_ago">fa 1 min</string>
|
<string name="minute_ago">fa 1 min</string>
|
||||||
<string name="minutes_ago">fa %d mins</string>
|
<string name="minutes_ago">fa %d mins</string>
|
||||||
<string name="unread_conversations">Converses sense llegir o no llegides</string>
|
|
||||||
<string name="sending">enviant…</string>
|
<string name="sending">enviant…</string>
|
||||||
<string name="message_decrypting">Desxifrant el missatge. Espereu…</string>
|
<string name="message_decrypting">Desxifrant el missatge. Espereu…</string>
|
||||||
<string name="pgp_message">Missatge xifrat amb OpenPGP</string>
|
<string name="pgp_message">Missatge xifrat amb OpenPGP</string>
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
<string name="just_now">právě teď</string>
|
<string name="just_now">právě teď</string>
|
||||||
<string name="minute_ago">před minutou</string>
|
<string name="minute_ago">před minutou</string>
|
||||||
<string name="minutes_ago">před %d minutami</string>
|
<string name="minutes_ago">před %d minutami</string>
|
||||||
<string name="unread_conversations">nepřečtené konverzace</string>
|
|
||||||
<string name="sending">odesílám…</string>
|
<string name="sending">odesílám…</string>
|
||||||
<string name="message_decrypting">Dešifrování zprávy. Chvíli strpení...</string>
|
<string name="message_decrypting">Dešifrování zprávy. Chvíli strpení...</string>
|
||||||
<string name="pgp_message">OpenPGP šifrovaná zpráva</string>
|
<string name="pgp_message">OpenPGP šifrovaná zpráva</string>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<string name="just_now">gerade</string>
|
<string name="just_now">gerade</string>
|
||||||
<string name="minute_ago">vor einer Minute</string>
|
<string name="minute_ago">vor einer Minute</string>
|
||||||
<string name="minutes_ago">vor %d Minuten</string>
|
<string name="minutes_ago">vor %d Minuten</string>
|
||||||
<string name="unread_conversations">ungelesene Unterhaltungen</string>
|
<string name="x_unread_conversations">%d ungelesene Unterhaltungen</string>
|
||||||
<string name="sending">senden…</string>
|
<string name="sending">senden…</string>
|
||||||
<string name="message_decrypting">Nachricht wird entschlüsselt. Bitte warten …</string>
|
<string name="message_decrypting">Nachricht wird entschlüsselt. Bitte warten …</string>
|
||||||
<string name="pgp_message">OpenPGP-verschlüsselte Nachricht</string>
|
<string name="pgp_message">OpenPGP-verschlüsselte Nachricht</string>
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<string name="just_now">μόλις τώρα</string>
|
<string name="just_now">μόλις τώρα</string>
|
||||||
<string name="minute_ago">πριν από 1 λεπτό</string>
|
<string name="minute_ago">πριν από 1 λεπτό</string>
|
||||||
<string name="minutes_ago">πριν από %d λεπτά</string>
|
<string name="minutes_ago">πριν από %d λεπτά</string>
|
||||||
<string name="unread_conversations">μη αναγνωσμένες Συζητήσεις</string>
|
|
||||||
<string name="sending">αποστολή...</string>
|
<string name="sending">αποστολή...</string>
|
||||||
<string name="message_decrypting">Αποκρυπτογράφηση μηνύματος. Παρακαλώ περιμένετε...</string>
|
<string name="message_decrypting">Αποκρυπτογράφηση μηνύματος. Παρακαλώ περιμένετε...</string>
|
||||||
<string name="pgp_message">OpenPGP κρυπτογραφημένο μήνυμα</string>
|
<string name="pgp_message">OpenPGP κρυπτογραφημένο μήνυμα</string>
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<string name="just_now">ahora</string>
|
<string name="just_now">ahora</string>
|
||||||
<string name="minute_ago">hace 1 min</string>
|
<string name="minute_ago">hace 1 min</string>
|
||||||
<string name="minutes_ago">hace %d min</string>
|
<string name="minutes_ago">hace %d min</string>
|
||||||
<string name="unread_conversations">conversaciones por leer</string>
|
|
||||||
<string name="sending">enviando…</string>
|
<string name="sending">enviando…</string>
|
||||||
<string name="message_decrypting">Descifrando mensaje. Por favor, espera...</string>
|
<string name="message_decrypting">Descifrando mensaje. Por favor, espera...</string>
|
||||||
<string name="pgp_message">Mensaje cifrado con OpenPGP</string>
|
<string name="pgp_message">Mensaje cifrado con OpenPGP</string>
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<string name="just_now">orain</string>
|
<string name="just_now">orain</string>
|
||||||
<string name="minute_ago">min 1 lehenago</string>
|
<string name="minute_ago">min 1 lehenago</string>
|
||||||
<string name="minutes_ago">%d min lehenago</string>
|
<string name="minutes_ago">%d min lehenago</string>
|
||||||
<string name="unread_conversations">irakurri gabeko elkarrizketak</string>
|
|
||||||
<string name="sending">bidaltzen…</string>
|
<string name="sending">bidaltzen…</string>
|
||||||
<string name="message_decrypting">Mezua desenkriptatzen. Mesedez itxaron…</string>
|
<string name="message_decrypting">Mezua desenkriptatzen. Mesedez itxaron…</string>
|
||||||
<string name="pgp_message">OpenPGPz enkriptatutako mezua</string>
|
<string name="pgp_message">OpenPGPz enkriptatutako mezua</string>
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
<string name="just_now">هم اکنون</string>
|
<string name="just_now">هم اکنون</string>
|
||||||
<string name="minute_ago">1 دقیقه قبل</string>
|
<string name="minute_ago">1 دقیقه قبل</string>
|
||||||
<string name="minutes_ago">%d دقیقه قبل</string>
|
<string name="minutes_ago">%d دقیقه قبل</string>
|
||||||
<string name="unread_conversations">گفتگو های خوانده نشده</string>
|
|
||||||
<string name="sending">در حال ارسال...</string>
|
<string name="sending">در حال ارسال...</string>
|
||||||
<string name="message_decrypting">در حال رمزگشایی پیام. لطفا صبور باشید...</string>
|
<string name="message_decrypting">در حال رمزگشایی پیام. لطفا صبور باشید...</string>
|
||||||
<string name="pgp_message">پیام رمز شده به وسیله OpenPGP</string>
|
<string name="pgp_message">پیام رمز شده به وسیله OpenPGP</string>
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
<string name="just_now">À l\'instant</string>
|
<string name="just_now">À l\'instant</string>
|
||||||
<string name="minute_ago">Il y a 1 minute</string>
|
<string name="minute_ago">Il y a 1 minute</string>
|
||||||
<string name="minutes_ago">Il y a %d minutes</string>
|
<string name="minutes_ago">Il y a %d minutes</string>
|
||||||
<string name="unread_conversations">Conversations non lues</string>
|
|
||||||
<string name="sending">Envoi…</string>
|
<string name="sending">Envoi…</string>
|
||||||
<string name="message_decrypting">Déchiffrement du message. Veuillez patienter...</string>
|
<string name="message_decrypting">Déchiffrement du message. Veuillez patienter...</string>
|
||||||
<string name="pgp_message">Message chiffré avec OpenPGP</string>
|
<string name="pgp_message">Message chiffré avec OpenPGP</string>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<string name="just_now">agora</string>
|
<string name="just_now">agora</string>
|
||||||
<string name="minute_ago">Hai 1 min</string>
|
<string name="minute_ago">Hai 1 min</string>
|
||||||
<string name="minutes_ago">hai %d minutos</string>
|
<string name="minutes_ago">hai %d minutos</string>
|
||||||
<string name="unread_conversations">conversas sen ler</string>
|
<string name="x_unread_conversations">%d conversas non lidas</string>
|
||||||
<string name="sending">enviando…</string>
|
<string name="sending">enviando…</string>
|
||||||
<string name="message_decrypting">Descifrando a mensaxe. Por favor agarde...</string>
|
<string name="message_decrypting">Descifrando a mensaxe. Por favor agarde...</string>
|
||||||
<string name="pgp_message">Mensaxe cifrado con OpenPGP</string>
|
<string name="pgp_message">Mensaxe cifrado con OpenPGP</string>
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<string name="just_now">Éppen most</string>
|
<string name="just_now">Éppen most</string>
|
||||||
<string name="minute_ago">1 perce</string>
|
<string name="minute_ago">1 perce</string>
|
||||||
<string name="minutes_ago">%d perce</string>
|
<string name="minutes_ago">%d perce</string>
|
||||||
<string name="unread_conversations">olvasatlan beszélgetés</string>
|
|
||||||
<string name="sending">küldés...</string>
|
<string name="sending">küldés...</string>
|
||||||
<string name="message_decrypting">Üzenet dekódolása. Kérem várjon...</string>
|
<string name="message_decrypting">Üzenet dekódolása. Kérem várjon...</string>
|
||||||
<string name="pgp_message">OpenPGP kódolású üzenet</string>
|
<string name="pgp_message">OpenPGP kódolású üzenet</string>
|
||||||
|
@ -871,4 +870,5 @@
|
||||||
<string name="not_a_backup_file">A kiválasztott fájl nem a Conversations biztonsági mentése</string>
|
<string name="not_a_backup_file">A kiválasztott fájl nem a Conversations biztonsági mentése</string>
|
||||||
<string name="account_already_setup">Ez a fiók már be lett állítva</string>
|
<string name="account_already_setup">Ez a fiók már be lett állítva</string>
|
||||||
<string name="please_enter_password">Kérem, adja meg a fiókhoz tartozó jelszót</string>
|
<string name="please_enter_password">Kérem, adja meg a fiókhoz tartozó jelszót</string>
|
||||||
|
<string name="unable_to_perform_this_action">Nem sikerült ezt a cselekvést elvégezni</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
<string name="just_now">sekarang</string>
|
<string name="just_now">sekarang</string>
|
||||||
<string name="minute_ago">1 min lalu</string>
|
<string name="minute_ago">1 min lalu</string>
|
||||||
<string name="minutes_ago">%d min lalu</string>
|
<string name="minutes_ago">%d min lalu</string>
|
||||||
<string name="unread_conversations">Percakapan belum dibaca</string>
|
|
||||||
<string name="sending">mengirim...</string>
|
<string name="sending">mengirim...</string>
|
||||||
<string name="message_decrypting">Mendekripsi pesan. Mohon tunggu…</string>
|
<string name="message_decrypting">Mendekripsi pesan. Mohon tunggu…</string>
|
||||||
<string name="pgp_message">Pesan terenkripsi OpenPGP</string>
|
<string name="pgp_message">Pesan terenkripsi OpenPGP</string>
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<string name="just_now">adesso</string>
|
<string name="just_now">adesso</string>
|
||||||
<string name="minute_ago">1 min fa</string>
|
<string name="minute_ago">1 min fa</string>
|
||||||
<string name="minutes_ago">%d min fa</string>
|
<string name="minutes_ago">%d min fa</string>
|
||||||
<string name="unread_conversations">Conversazioni non lette</string>
|
|
||||||
<string name="sending">invio…</string>
|
<string name="sending">invio…</string>
|
||||||
<string name="message_decrypting">Decifrazione messaggio. Attendere prego...</string>
|
<string name="message_decrypting">Decifrazione messaggio. Attendere prego...</string>
|
||||||
<string name="pgp_message">Messaggio cifrato con OpenPGP</string>
|
<string name="pgp_message">Messaggio cifrato con OpenPGP</string>
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
<string name="just_now">ממש עכשיו</string>
|
<string name="just_now">ממש עכשיו</string>
|
||||||
<string name="minute_ago">לפני דקה</string>
|
<string name="minute_ago">לפני דקה</string>
|
||||||
<string name="minutes_ago">לפני %d דקות</string>
|
<string name="minutes_ago">לפני %d דקות</string>
|
||||||
<string name="unread_conversations">שיחות שלא נקראו</string>
|
|
||||||
<string name="sending">שולח...</string>
|
<string name="sending">שולח...</string>
|
||||||
<string name="message_decrypting">כעת מפענח צופן הודעה. אנא המתן…</string>
|
<string name="message_decrypting">כעת מפענח צופן הודעה. אנא המתן…</string>
|
||||||
<string name="pgp_message">הודעה מוצפנת OpenPGP</string>
|
<string name="pgp_message">הודעה מוצפנת OpenPGP</string>
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
<string name="just_now">ちょうど今</string>
|
<string name="just_now">ちょうど今</string>
|
||||||
<string name="minute_ago">1 分前</string>
|
<string name="minute_ago">1 分前</string>
|
||||||
<string name="minutes_ago">%d 分前</string>
|
<string name="minutes_ago">%d 分前</string>
|
||||||
<string name="unread_conversations">未読の会話</string>
|
|
||||||
<string name="sending">送信中…</string>
|
<string name="sending">送信中…</string>
|
||||||
<string name="message_decrypting">メッセージを復号化しています。しばらくお待ちください…</string>
|
<string name="message_decrypting">メッセージを復号化しています。しばらくお待ちください…</string>
|
||||||
<string name="pgp_message">OpenPGP 暗号化メッセージ</string>
|
<string name="pgp_message">OpenPGP 暗号化メッセージ</string>
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
<string name="just_now">방금 </string>
|
<string name="just_now">방금 </string>
|
||||||
<string name="minute_ago">1분 전 </string>
|
<string name="minute_ago">1분 전 </string>
|
||||||
<string name="minutes_ago">%d 분 전 </string>
|
<string name="minutes_ago">%d 분 전 </string>
|
||||||
<string name="unread_conversations">읽지 않은 대화 </string>
|
|
||||||
<string name="sending">보내는중... </string>
|
<string name="sending">보내는중... </string>
|
||||||
<string name="message_decrypting">메세지 복호화중입니다. 기다리세요...</string>
|
<string name="message_decrypting">메세지 복호화중입니다. 기다리세요...</string>
|
||||||
<string name="pgp_message">OpenPGP로 암호화된 메세지</string>
|
<string name="pgp_message">OpenPGP로 암호화된 메세지</string>
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
<string name="just_now">akkurat nå</string>
|
<string name="just_now">akkurat nå</string>
|
||||||
<string name="minute_ago">1 minutt siden</string>
|
<string name="minute_ago">1 minutt siden</string>
|
||||||
<string name="minutes_ago">%d minutter siden</string>
|
<string name="minutes_ago">%d minutter siden</string>
|
||||||
<string name="unread_conversations">uleste samtaler</string>
|
|
||||||
<string name="sending">sender...</string>
|
<string name="sending">sender...</string>
|
||||||
<string name="message_decrypting">Dekrypterer melding mens du venter.</string>
|
<string name="message_decrypting">Dekrypterer melding mens du venter.</string>
|
||||||
<string name="pgp_message">OpenPGP-kryptert melding</string>
|
<string name="pgp_message">OpenPGP-kryptert melding</string>
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<string name="just_now">zojuist</string>
|
<string name="just_now">zojuist</string>
|
||||||
<string name="minute_ago">1 min. geleden</string>
|
<string name="minute_ago">1 min. geleden</string>
|
||||||
<string name="minutes_ago">%d min. geleden</string>
|
<string name="minutes_ago">%d min. geleden</string>
|
||||||
<string name="unread_conversations">ongelezen gesprekken</string>
|
|
||||||
<string name="sending">versturen…</string>
|
<string name="sending">versturen…</string>
|
||||||
<string name="message_decrypting">Bericht aan het ontsleutelen. Even geduld…</string>
|
<string name="message_decrypting">Bericht aan het ontsleutelen. Even geduld…</string>
|
||||||
<string name="pgp_message">OpenPGP-versleuteld bericht</string>
|
<string name="pgp_message">OpenPGP-versleuteld bericht</string>
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<string name="just_now">przed chwilą</string>
|
<string name="just_now">przed chwilą</string>
|
||||||
<string name="minute_ago">minutę temu</string>
|
<string name="minute_ago">minutę temu</string>
|
||||||
<string name="minutes_ago">%d minut temu</string>
|
<string name="minutes_ago">%d minut temu</string>
|
||||||
<string name="unread_conversations">nieprzeczytanych konwersacji</string>
|
|
||||||
<string name="sending">wysyłanie...</string>
|
<string name="sending">wysyłanie...</string>
|
||||||
<string name="message_decrypting">Odszyfrowywanie wiadomości. To zajmie tylko chwilę...</string>
|
<string name="message_decrypting">Odszyfrowywanie wiadomości. To zajmie tylko chwilę...</string>
|
||||||
<string name="pgp_message">Wiadomość zaszyfrowana OpenPGP</string>
|
<string name="pgp_message">Wiadomość zaszyfrowana OpenPGP</string>
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
<string name="action_unblock_contact">Desbloquear contato</string>
|
<string name="action_unblock_contact">Desbloquear contato</string>
|
||||||
<string name="action_block_domain">Bloquear domínio</string>
|
<string name="action_block_domain">Bloquear domínio</string>
|
||||||
<string name="action_unblock_domain">Desbloquear domínio</string>
|
<string name="action_unblock_domain">Desbloquear domínio</string>
|
||||||
|
<string name="action_block_participant">Bloquear participante</string>
|
||||||
|
<string name="action_unblock_participant">Desbloquear participante</string>
|
||||||
<string name="title_activity_manage_accounts">Gerenciar contas</string>
|
<string name="title_activity_manage_accounts">Gerenciar contas</string>
|
||||||
<string name="title_activity_settings">Configurações</string>
|
<string name="title_activity_settings">Configurações</string>
|
||||||
<string name="title_activity_sharewith">Compartilhar com a conversa</string>
|
<string name="title_activity_sharewith">Compartilhar com a conversa</string>
|
||||||
|
@ -28,7 +30,7 @@
|
||||||
<string name="just_now">agora</string>
|
<string name="just_now">agora</string>
|
||||||
<string name="minute_ago">1 minuto atrás</string>
|
<string name="minute_ago">1 minuto atrás</string>
|
||||||
<string name="minutes_ago">%d minutos atrás</string>
|
<string name="minutes_ago">%d minutos atrás</string>
|
||||||
<string name="unread_conversations">conversas não lidas</string>
|
<string name="x_unread_conversations">%d conversas não lidas</string>
|
||||||
<string name="sending">enviando...</string>
|
<string name="sending">enviando...</string>
|
||||||
<string name="message_decrypting">Descriptografando a mensagem. Por favor, aguarde...</string>
|
<string name="message_decrypting">Descriptografando a mensagem. Por favor, aguarde...</string>
|
||||||
<string name="pgp_message">Mensagem criptografada via OpenPGP</string>
|
<string name="pgp_message">Mensagem criptografada via OpenPGP</string>
|
||||||
|
@ -861,4 +863,12 @@
|
||||||
<string name="this_looks_like_a_domain">Isso se parece com um endereço de domínio</string>
|
<string name="this_looks_like_a_domain">Isso se parece com um endereço de domínio</string>
|
||||||
<string name="add_anway">Adicionar mesmo assim</string>
|
<string name="add_anway">Adicionar mesmo assim</string>
|
||||||
<string name="this_looks_like_channel">Isso se parece com um endereço de canal</string>
|
<string name="this_looks_like_channel">Isso se parece com um endereço de canal</string>
|
||||||
|
<string name="share_backup_files">Compartilhar arquivos de backup</string>
|
||||||
|
<string name="conversations_backup">Backup do Conversations</string>
|
||||||
|
<string name="event">Evento</string>
|
||||||
|
<string name="open_backup">Abrir backup</string>
|
||||||
|
<string name="not_a_backup_file">O arquivo que você selecionou não é um backup do Conversations</string>
|
||||||
|
<string name="account_already_setup">Esta conta já foi configurada</string>
|
||||||
|
<string name="please_enter_password">Por favor, digite a senha para esta conta</string>
|
||||||
|
<string name="unable_to_perform_this_action">Não foi possível executar essa ação</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
<string name="just_now">agora há pouco</string>
|
<string name="just_now">agora há pouco</string>
|
||||||
<string name="minute_ago">1 minuto atrás</string>
|
<string name="minute_ago">1 minuto atrás</string>
|
||||||
<string name="minutes_ago">%d minutos atrás</string>
|
<string name="minutes_ago">%d minutos atrás</string>
|
||||||
<string name="unread_conversations">Conversas não lidas</string>
|
|
||||||
<string name="sending">enviando...</string>
|
<string name="sending">enviando...</string>
|
||||||
<string name="message_decrypting">Decifrando a mensagem. Por favor aguarde...</string>
|
<string name="message_decrypting">Decifrando a mensagem. Por favor aguarde...</string>
|
||||||
<string name="pgp_message">Mensagem cifrada OpenPGP</string>
|
<string name="pgp_message">Mensagem cifrada OpenPGP</string>
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<string name="just_now">în acest moment</string>
|
<string name="just_now">în acest moment</string>
|
||||||
<string name="minute_ago">acum un minut</string>
|
<string name="minute_ago">acum un minut</string>
|
||||||
<string name="minutes_ago">acum %d minute</string>
|
<string name="minutes_ago">acum %d minute</string>
|
||||||
<string name="unread_conversations">conversații necitite</string>
|
|
||||||
<string name="sending">trimitere...</string>
|
<string name="sending">trimitere...</string>
|
||||||
<string name="message_decrypting">Decriptez mesaj. Te rog așteaptă...</string>
|
<string name="message_decrypting">Decriptez mesaj. Te rog așteaptă...</string>
|
||||||
<string name="pgp_message">Mesaj criptat cu OpenPGP</string>
|
<string name="pgp_message">Mesaj criptat cu OpenPGP</string>
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
<string name="just_now">только что</string>
|
<string name="just_now">только что</string>
|
||||||
<string name="minute_ago">1 минуту назад</string>
|
<string name="minute_ago">1 минуту назад</string>
|
||||||
<string name="minutes_ago">%d мин. назад</string>
|
<string name="minutes_ago">%d мин. назад</string>
|
||||||
<string name="unread_conversations">сообщен. не прочитано</string>
|
|
||||||
<string name="sending">отправка…</string>
|
<string name="sending">отправка…</string>
|
||||||
<string name="message_decrypting">Расшифровка сообщения. Подождите…</string>
|
<string name="message_decrypting">Расшифровка сообщения. Подождите…</string>
|
||||||
<string name="pgp_message">OpenPGP зашифр. сообщение</string>
|
<string name="pgp_message">OpenPGP зашифр. сообщение</string>
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
<string name="just_now">práve teraz</string>
|
<string name="just_now">práve teraz</string>
|
||||||
<string name="minute_ago">pred 1 minútou</string>
|
<string name="minute_ago">pred 1 minútou</string>
|
||||||
<string name="minutes_ago">pred %d minútami</string>
|
<string name="minutes_ago">pred %d minútami</string>
|
||||||
<string name="unread_conversations">neprečítané konverzácie</string>
|
|
||||||
<string name="sending">posielam...</string>
|
<string name="sending">posielam...</string>
|
||||||
<string name="nick_in_use">Prezývka už existuje</string>
|
<string name="nick_in_use">Prezývka už existuje</string>
|
||||||
<string name="admin">Administrátor</string>
|
<string name="admin">Administrátor</string>
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
<string name="just_now">управо сад</string>
|
<string name="just_now">управо сад</string>
|
||||||
<string name="minute_ago">пре минут</string>
|
<string name="minute_ago">пре минут</string>
|
||||||
<string name="minutes_ago">пре %d минута</string>
|
<string name="minutes_ago">пре %d минута</string>
|
||||||
<string name="unread_conversations">непрочитане преписке</string>
|
|
||||||
<string name="sending">шаљем…</string>
|
<string name="sending">шаљем…</string>
|
||||||
<string name="message_decrypting">Дешифрујем поруку, сачекајте…</string>
|
<string name="message_decrypting">Дешифрујем поруку, сачекајте…</string>
|
||||||
<string name="pgp_message">ОпенПГП шифрована порука</string>
|
<string name="pgp_message">ОпенПГП шифрована порука</string>
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
<string name="just_now">just nu</string>
|
<string name="just_now">just nu</string>
|
||||||
<string name="minute_ago">1 min sedan</string>
|
<string name="minute_ago">1 min sedan</string>
|
||||||
<string name="minutes_ago">%d min sedan</string>
|
<string name="minutes_ago">%d min sedan</string>
|
||||||
<string name="unread_conversations">olästa konversationer</string>
|
|
||||||
<string name="sending">skickar…</string>
|
<string name="sending">skickar…</string>
|
||||||
<string name="message_decrypting">Avkrypterar meddelande. Vänta…</string>
|
<string name="message_decrypting">Avkrypterar meddelande. Vänta…</string>
|
||||||
<string name="pgp_message">OpenPGP-krypterat meddelande</string>
|
<string name="pgp_message">OpenPGP-krypterat meddelande</string>
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
<string name="just_now">şimdi</string>
|
<string name="just_now">şimdi</string>
|
||||||
<string name="minute_ago">1 dakika önce</string>
|
<string name="minute_ago">1 dakika önce</string>
|
||||||
<string name="minutes_ago">%d dakika önc</string>
|
<string name="minutes_ago">%d dakika önc</string>
|
||||||
<string name="unread_conversations">okunmamış konuşmalar</string>
|
|
||||||
<string name="sending">gönderiyor…</string>
|
<string name="sending">gönderiyor…</string>
|
||||||
<string name="message_decrypting">İleti deşifre ediliyor. Lütfen bekleyin…</string>
|
<string name="message_decrypting">İleti deşifre ediliyor. Lütfen bekleyin…</string>
|
||||||
<string name="pgp_message">OpenPGP şifreli ileti</string>
|
<string name="pgp_message">OpenPGP şifreli ileti</string>
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
<string name="just_now">щойно</string>
|
<string name="just_now">щойно</string>
|
||||||
<string name="minute_ago">1 хвилину тому</string>
|
<string name="minute_ago">1 хвилину тому</string>
|
||||||
<string name="minutes_ago">%d хвилин тому</string>
|
<string name="minutes_ago">%d хвилин тому</string>
|
||||||
<string name="unread_conversations">не переглянуті розмови </string>
|
|
||||||
<string name="sending">відправляю…</string>
|
<string name="sending">відправляю…</string>
|
||||||
<string name="message_decrypting">Розшифровую повідомлення. Зачекайте, будь ласка…</string>
|
<string name="message_decrypting">Розшифровую повідомлення. Зачекайте, будь ласка…</string>
|
||||||
<string name="pgp_message">Повідомлення, зашифроване OpenPGP</string>
|
<string name="pgp_message">Повідомлення, зашифроване OpenPGP</string>
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
<string name="just_now">mới đây</string>
|
<string name="just_now">mới đây</string>
|
||||||
<string name="minute_ago">1 phút trước</string>
|
<string name="minute_ago">1 phút trước</string>
|
||||||
<string name="minutes_ago">%d phút trước</string>
|
<string name="minutes_ago">%d phút trước</string>
|
||||||
<string name="unread_conversations">Các hội thoại chưa đọc</string>
|
|
||||||
<string name="sending">đang gửi...</string>
|
<string name="sending">đang gửi...</string>
|
||||||
<string name="message_decrypting">Đang giải mã tin nhắn. Xin chờ...</string>
|
<string name="message_decrypting">Đang giải mã tin nhắn. Xin chờ...</string>
|
||||||
<string name="pgp_message">Tin nhắn mã hoá bằng OpenPGP</string>
|
<string name="pgp_message">Tin nhắn mã hoá bằng OpenPGP</string>
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
<string name="just_now">刚刚</string>
|
<string name="just_now">刚刚</string>
|
||||||
<string name="minute_ago">1分钟前</string>
|
<string name="minute_ago">1分钟前</string>
|
||||||
<string name="minutes_ago">%d分钟前</string>
|
<string name="minutes_ago">%d分钟前</string>
|
||||||
<string name="unread_conversations">未读会话</string>
|
|
||||||
<string name="sending">正在发送…</string>
|
<string name="sending">正在发送…</string>
|
||||||
<string name="message_decrypting">解密信息中. 请稍候…</string>
|
<string name="message_decrypting">解密信息中. 请稍候…</string>
|
||||||
<string name="pgp_message">OpenPGP 加密的信息</string>
|
<string name="pgp_message">OpenPGP 加密的信息</string>
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
<string name="just_now">剛剛</string>
|
<string name="just_now">剛剛</string>
|
||||||
<string name="minute_ago">1 分鐘前</string>
|
<string name="minute_ago">1 分鐘前</string>
|
||||||
<string name="minutes_ago">%d分鐘前</string>
|
<string name="minutes_ago">%d分鐘前</string>
|
||||||
<string name="unread_conversations">未讀會話</string>
|
|
||||||
<string name="sending">正在發送…</string>
|
<string name="sending">正在發送…</string>
|
||||||
<string name="message_decrypting">訊息解密中,請稍候…</string>
|
<string name="message_decrypting">訊息解密中,請稍候…</string>
|
||||||
<string name="pgp_message">OpenPGP 加密的信息</string>
|
<string name="pgp_message">OpenPGP 加密的信息</string>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<string name="just_now">just now</string>
|
<string name="just_now">just now</string>
|
||||||
<string name="minute_ago">1 min ago</string>
|
<string name="minute_ago">1 min ago</string>
|
||||||
<string name="minutes_ago">%d mins ago</string>
|
<string name="minutes_ago">%d mins ago</string>
|
||||||
<string name="unread_conversations">unread Conversations</string>
|
<string name="x_unread_conversations">%d unread conversations</string>
|
||||||
<string name="sending">sending…</string>
|
<string name="sending">sending…</string>
|
||||||
<string name="message_decrypting">Decrypting message. Please wait…</string>
|
<string name="message_decrypting">Decrypting message. Please wait…</string>
|
||||||
<string name="pgp_message">OpenPGP encrypted message</string>
|
<string name="pgp_message">OpenPGP encrypted message</string>
|
||||||
|
|
|
@ -19,4 +19,8 @@
|
||||||
<string name="no_microphone_permission">O Quicksy necessita de acesso ao microfone</string>
|
<string name="no_microphone_permission">O Quicksy necessita de acesso ao microfone</string>
|
||||||
<string name="foreground_service_channel_description">Essa categoria de notificação é utilizada para exibir uma notificação permanente indicando que o Quicksy está em execução.</string>
|
<string name="foreground_service_channel_description">Essa categoria de notificação é utilizada para exibir uma notificação permanente indicando que o Quicksy está em execução.</string>
|
||||||
<string name="set_profile_picture">Imagem de perfil do Quicksy</string>
|
<string name="set_profile_picture">Imagem de perfil do Quicksy</string>
|
||||||
|
<string name="not_available_in_your_country">Quicksy agora está disponível no seu país.</string>
|
||||||
|
<string name="unable_to_verify_server_identity">Não foi possível verificar a identidade do servidor.</string>
|
||||||
|
<string name="unknown_security_error">Erro de segurança desconhecido.</string>
|
||||||
|
<string name="timeout_while_connecting_to_server">Tempo esgotado ao tentar conectar ao servidor.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue