Merge tag '2.9.4' into develop
This commit is contained in:
commit
d2cd482a07
|
@ -11,7 +11,7 @@ android:
|
||||||
- '.+'
|
- '.+'
|
||||||
before_script:
|
before_script:
|
||||||
- mkdir libs
|
- mkdir libs
|
||||||
- wget -O libs/libwebrtc-m85.aar https://gultsch.de/files/libwebrtc-m85.aar
|
- wget -O libs/libwebrtc-m87.aar https://gultsch.de/files/libwebrtc-m87.aar
|
||||||
script:
|
script:
|
||||||
- ./gradlew assembleConversationsFreeSystemRelease
|
- ./gradlew assembleConversationsFreeSystemRelease
|
||||||
- ./gradlew assembleQuicksyFreeCompatRelease
|
- ./gradlew assembleQuicksyFreeCompatRelease
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
### Version 2.9.4
|
||||||
|
* minor stability improvements for A/V calls
|
||||||
|
|
||||||
|
### Version 2.9.3
|
||||||
|
|
||||||
|
* Fixed connectivity issues when different accounts used different SCRAM mechanisms
|
||||||
|
* Add support for SCRAM-SHA-512
|
||||||
|
* Allow P2P (Jingle) file transfer with self contact
|
||||||
|
|
||||||
### Version 2.9.2
|
### Version 2.9.2
|
||||||
|
|
||||||
* Offer Easy Invite generation on supporting servers
|
* Offer Easy Invite generation on supporting servers
|
||||||
|
|
|
@ -80,7 +80,7 @@ dependencies {
|
||||||
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
|
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
|
||||||
implementation 'com.google.guava:guava:27.1-android'
|
implementation 'com.google.guava:guava:27.1-android'
|
||||||
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1'
|
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1'
|
||||||
//implementation fileTree(include: ['libwebrtc-m85.aar'], dir: 'libs')
|
//implementation fileTree(include: ['libwebrtc-m87.aar'], dir: 'libs')
|
||||||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,8 +96,8 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 402
|
versionCode 404
|
||||||
versionName "2.9.2"
|
versionName "2.9.4"
|
||||||
archivesBaseName += "-$versionName"
|
archivesBaseName += "-$versionName"
|
||||||
applicationId "eu.sum7.conversations"
|
applicationId "eu.sum7.conversations"
|
||||||
resValue "string", "applicationId", applicationId
|
resValue "string", "applicationId", applicationId
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<?xml-stylesheet href="../style.xsl" type="text/xsl"?>
|
<?xml-stylesheet href="../style.xsl" type="text/xsl"?>
|
||||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
<Project xmlns="http://usefulinc.com/ns/doap#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#">
|
<Project xmlns="http://usefulinc.com/ns/doap#"
|
||||||
|
xmlns:foaf="http://xmlns.com/foaf/0.1/"
|
||||||
|
xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#"
|
||||||
|
xmlns:schema="https://schema.org/">
|
||||||
<name>Conversations</name>
|
<name>Conversations</name>
|
||||||
|
|
||||||
<created>2014-01-14</created>
|
<created>2014-01-14</created>
|
||||||
|
@ -22,13 +25,12 @@
|
||||||
<!-- See https://github.com/ewilderj/doap/issues/49 -->
|
<!-- See https://github.com/ewilderj/doap/issues/49 -->
|
||||||
<language>en</language>
|
<language>en</language>
|
||||||
|
|
||||||
<logo rdf:resource="https://raw.githubusercontent.com/iNPUTmice/Conversations/master/doap.rdf"/>
|
<schema:logo rdf:resource="https://raw.githubusercontent.com/iNPUTmice/Conversations/master/art/ic_launcher.svg"/>
|
||||||
|
|
||||||
<programming-language>Java</programming-language>
|
<programming-language>Java</programming-language>
|
||||||
|
|
||||||
<os>Android</os>
|
<os>Android</os>
|
||||||
|
|
||||||
<!-- TODO: Categories are URIs, find a better location for them. -->
|
|
||||||
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-xmpp"/>
|
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-xmpp"/>
|
||||||
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-jabber"/>
|
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-jabber"/>
|
||||||
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-client"/>
|
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-client"/>
|
|
@ -0,0 +1,4 @@
|
||||||
|
• Fixed connectivity issues when different accounts used different SCRAM mechanisms
|
||||||
|
• Add support for SCRAM-SHA-512
|
||||||
|
• Allow P2P (Jingle) file transfer with self contact
|
||||||
|
• minor stability improvements for A/V calls
|
|
@ -11,4 +11,8 @@ In ogni caso per facilitare puoi creare facilmente un account su chat.sum7.eu, u
|
||||||
<string name="magic_create_text_fixed">Sei stato invitato su %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un account.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>
|
<string name="magic_create_text_fixed">Sei stato invitato su %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un account.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>
|
||||||
<string name="your_server_invitation">Il tuo invito al server</string>
|
<string name="your_server_invitation">Il tuo invito al server</string>
|
||||||
<string name="improperly_formatted_provisioning">Codice di approvvigionamento formattato male</string>
|
<string name="improperly_formatted_provisioning">Codice di approvvigionamento formattato male</string>
|
||||||
</resources>
|
<string name="tap_share_button_send_invite">Tocca il pulsante condividi per inviare al contatto un invito per %1$s.</string>
|
||||||
|
<string name="if_contact_is_nearby_use_qr">Se il contatto è vicino, può anche scansionare il codice sottostante per accettare il tuo invito.</string>
|
||||||
|
<string name="easy_invite_share_text">Unisciti a %1$s e chatta con me: %2$s</string>
|
||||||
|
<string name="share_invite_with">Condividi invito con...</string>
|
||||||
|
</resources>
|
||||||
|
|
|
@ -9,4 +9,8 @@
|
||||||
<string name="magic_create_text_fixed">Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP.</string>
|
<string name="magic_create_text_fixed">Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP.</string>
|
||||||
<string name="your_server_invitation">Zaproszenie twojego serwera</string>
|
<string name="your_server_invitation">Zaproszenie twojego serwera</string>
|
||||||
<string name="improperly_formatted_provisioning">Niepoprawnie sformatowany kod zaopatrywania</string>
|
<string name="improperly_formatted_provisioning">Niepoprawnie sformatowany kod zaopatrywania</string>
|
||||||
</resources>
|
<string name="tap_share_button_send_invite">Użyj przycisku udostępniania aby wysłać swojemu kontaktowi zaproszenie do %1$s.</string>
|
||||||
|
<string name="if_contact_is_nearby_use_qr">Jeśli twój kontakt jest blisko może przeskanować kod poniżej aby zaakceptować twoje zaproszenie.</string>
|
||||||
|
<string name="easy_invite_share_text">Dołącz do %1$s aby porozmawiać ze mną: %2$s</string>
|
||||||
|
<string name="share_invite_with">Udostępnij zaproszenie...</string>
|
||||||
|
</resources>
|
||||||
|
|
|
@ -9,4 +9,8 @@
|
||||||
<string name="magic_create_text_fixed">Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.</string>
|
<string name="magic_create_text_fixed">Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.</string>
|
||||||
<string name="your_server_invitation">Seu convite do servidor</string>
|
<string name="your_server_invitation">Seu convite do servidor</string>
|
||||||
<string name="improperly_formatted_provisioning">Código de provisionamento formatado de maneira imprópria</string>
|
<string name="improperly_formatted_provisioning">Código de provisionamento formatado de maneira imprópria</string>
|
||||||
</resources>
|
<string name="tap_share_button_send_invite">Toque no botão compartilhar para enviar, para seu contato, um convite para %1$s.</string>
|
||||||
|
<string name="if_contact_is_nearby_use_qr">Se seu contato estiver por perto, ele também pode escanear o código abaixo para aceitar seu convite.</string>
|
||||||
|
<string name="easy_invite_share_text">Junte-se a %1$s e converse comigo: %2$s</string>
|
||||||
|
<string name="share_invite_with">Compartilhe o convite com...</string>
|
||||||
|
</resources>
|
||||||
|
|
|
@ -7,22 +7,24 @@ import eu.siacs.conversations.xml.TagWriter;
|
||||||
|
|
||||||
public class Anonymous extends SaslMechanism {
|
public class Anonymous extends SaslMechanism {
|
||||||
|
|
||||||
public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) {
|
public static final String MECHANISM = "ANONYMOUS";
|
||||||
super(tagWriter, account, rng);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) {
|
||||||
public int getPriority() {
|
super(tagWriter, account, rng);
|
||||||
return 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMechanism() {
|
public int getPriority() {
|
||||||
return "ANONYMOUS";
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientFirstMessage() {
|
public String getMechanism() {
|
||||||
return "";
|
return MECHANISM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientFirstMessage() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,79 +12,82 @@ import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.xml.TagWriter;
|
import eu.siacs.conversations.xml.TagWriter;
|
||||||
|
|
||||||
public class DigestMd5 extends SaslMechanism {
|
public class DigestMd5 extends SaslMechanism {
|
||||||
public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
|
||||||
super(tagWriter, account, rng);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public static final String MECHANISM = "DIGEST-MD5";
|
||||||
public int getPriority() {
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||||
public String getMechanism() {
|
super(tagWriter, account, rng);
|
||||||
return "DIGEST-MD5";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private State state = State.INITIAL;
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getResponse(final String challenge) throws AuthenticationException {
|
public String getMechanism() {
|
||||||
switch (state) {
|
return MECHANISM;
|
||||||
case INITIAL:
|
}
|
||||||
state = State.RESPONSE_SENT;
|
|
||||||
final String encodedResponse;
|
|
||||||
try {
|
|
||||||
final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
|
|
||||||
String nonce = "";
|
|
||||||
for (final String token : tokenizer) {
|
|
||||||
final String[] parts = token.split("=", 2);
|
|
||||||
if (parts[0].equals("nonce")) {
|
|
||||||
nonce = parts[1].replace("\"", "");
|
|
||||||
} else if (parts[0].equals("rspauth")) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final String digestUri = "xmpp/" + account.getServer();
|
|
||||||
final String nonceCount = "00000001";
|
|
||||||
final String x = account.getUsername() + ":" + account.getServer() + ":"
|
|
||||||
+ account.getPassword();
|
|
||||||
final MessageDigest md = MessageDigest.getInstance("MD5");
|
|
||||||
final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
|
|
||||||
final String cNonce = CryptoHelper.random(100,rng);
|
|
||||||
final byte[] a1 = CryptoHelper.concatenateByteArrays(y,
|
|
||||||
(":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset()));
|
|
||||||
final String a2 = "AUTHENTICATE:" + digestUri;
|
|
||||||
final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
|
|
||||||
final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset
|
|
||||||
.defaultCharset())));
|
|
||||||
final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
|
|
||||||
+ ":auth:" + ha2;
|
|
||||||
final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset
|
|
||||||
.defaultCharset())));
|
|
||||||
final String saslString = "username=\"" + account.getUsername()
|
|
||||||
+ "\",realm=\"" + account.getServer() + "\",nonce=\""
|
|
||||||
+ nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
|
|
||||||
+ ",qop=auth,digest-uri=\"" + digestUri + "\",response="
|
|
||||||
+ response + ",charset=utf-8";
|
|
||||||
encodedResponse = Base64.encodeToString(
|
|
||||||
saslString.getBytes(Charset.defaultCharset()),
|
|
||||||
Base64.NO_WRAP);
|
|
||||||
} catch (final NoSuchAlgorithmException e) {
|
|
||||||
throw new AuthenticationException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return encodedResponse;
|
private State state = State.INITIAL;
|
||||||
case RESPONSE_SENT:
|
|
||||||
state = State.VALID_SERVER_RESPONSE;
|
@Override
|
||||||
break;
|
public String getResponse(final String challenge) throws AuthenticationException {
|
||||||
case VALID_SERVER_RESPONSE:
|
switch (state) {
|
||||||
if (challenge==null) {
|
case INITIAL:
|
||||||
return null; //everything is fine
|
state = State.RESPONSE_SENT;
|
||||||
}
|
final String encodedResponse;
|
||||||
default:
|
try {
|
||||||
throw new InvalidStateException(state);
|
final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
|
||||||
}
|
String nonce = "";
|
||||||
return null;
|
for (final String token : tokenizer) {
|
||||||
}
|
final String[] parts = token.split("=", 2);
|
||||||
|
if (parts[0].equals("nonce")) {
|
||||||
|
nonce = parts[1].replace("\"", "");
|
||||||
|
} else if (parts[0].equals("rspauth")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String digestUri = "xmpp/" + account.getServer();
|
||||||
|
final String nonceCount = "00000001";
|
||||||
|
final String x = account.getUsername() + ":" + account.getServer() + ":"
|
||||||
|
+ account.getPassword();
|
||||||
|
final MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
|
||||||
|
final String cNonce = CryptoHelper.random(100, rng);
|
||||||
|
final byte[] a1 = CryptoHelper.concatenateByteArrays(y,
|
||||||
|
(":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset()));
|
||||||
|
final String a2 = "AUTHENTICATE:" + digestUri;
|
||||||
|
final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
|
||||||
|
final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset
|
||||||
|
.defaultCharset())));
|
||||||
|
final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
|
||||||
|
+ ":auth:" + ha2;
|
||||||
|
final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset
|
||||||
|
.defaultCharset())));
|
||||||
|
final String saslString = "username=\"" + account.getUsername()
|
||||||
|
+ "\",realm=\"" + account.getServer() + "\",nonce=\""
|
||||||
|
+ nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
|
||||||
|
+ ",qop=auth,digest-uri=\"" + digestUri + "\",response="
|
||||||
|
+ response + ",charset=utf-8";
|
||||||
|
encodedResponse = Base64.encodeToString(
|
||||||
|
saslString.getBytes(Charset.defaultCharset()),
|
||||||
|
Base64.NO_WRAP);
|
||||||
|
} catch (final NoSuchAlgorithmException e) {
|
||||||
|
throw new AuthenticationException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodedResponse;
|
||||||
|
case RESPONSE_SENT:
|
||||||
|
state = State.VALID_SERVER_RESPONSE;
|
||||||
|
break;
|
||||||
|
case VALID_SERVER_RESPONSE:
|
||||||
|
if (challenge == null) {
|
||||||
|
return null; //everything is fine
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new InvalidStateException(state);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.siacs.conversations.crypto.sasl;
|
package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
@ -8,22 +9,24 @@ import eu.siacs.conversations.xml.TagWriter;
|
||||||
|
|
||||||
public class External extends SaslMechanism {
|
public class External extends SaslMechanism {
|
||||||
|
|
||||||
public External(TagWriter tagWriter, Account account, SecureRandom rng) {
|
public static final String MECHANISM = "EXTERNAL";
|
||||||
super(tagWriter, account, rng);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public External(TagWriter tagWriter, Account account, SecureRandom rng) {
|
||||||
public int getPriority() {
|
super(tagWriter, account, rng);
|
||||||
return 25;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMechanism() {
|
public int getPriority() {
|
||||||
return "EXTERNAL";
|
return 25;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientFirstMessage() {
|
public String getMechanism() {
|
||||||
return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(),Base64.NO_WRAP);
|
return MECHANISM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientFirstMessage() {
|
||||||
|
return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,27 +8,30 @@ import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.xml.TagWriter;
|
import eu.siacs.conversations.xml.TagWriter;
|
||||||
|
|
||||||
public class Plain extends SaslMechanism {
|
public class Plain extends SaslMechanism {
|
||||||
public Plain(final TagWriter tagWriter, final Account account) {
|
|
||||||
super(tagWriter, account, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public static final String MECHANISM = "PLAIN";
|
||||||
public int getPriority() {
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public Plain(final TagWriter tagWriter, final Account account) {
|
||||||
public String getMechanism() {
|
super(tagWriter, account, null);
|
||||||
return "PLAIN";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientFirstMessage() {
|
public int getPriority() {
|
||||||
return getMessage(account.getUsername(), account.getPassword());
|
return 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getMessage(String username, String password) {
|
@Override
|
||||||
final String message = '\u0000' + username + '\u0000' + password;
|
public String getMechanism() {
|
||||||
return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
|
return MECHANISM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientFirstMessage() {
|
||||||
|
return getMessage(account.getUsername(), account.getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getMessage(String username, String password) {
|
||||||
|
final String message = '\u0000' + username + '\u0000' + password;
|
||||||
|
return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,60 +7,63 @@ import eu.siacs.conversations.xml.TagWriter;
|
||||||
|
|
||||||
public abstract class SaslMechanism {
|
public abstract class SaslMechanism {
|
||||||
|
|
||||||
final protected TagWriter tagWriter;
|
final protected TagWriter tagWriter;
|
||||||
final protected Account account;
|
final protected Account account;
|
||||||
final protected SecureRandom rng;
|
final protected SecureRandom rng;
|
||||||
|
|
||||||
protected enum State {
|
protected enum State {
|
||||||
INITIAL,
|
INITIAL,
|
||||||
AUTH_TEXT_SENT,
|
AUTH_TEXT_SENT,
|
||||||
RESPONSE_SENT,
|
RESPONSE_SENT,
|
||||||
VALID_SERVER_RESPONSE,
|
VALID_SERVER_RESPONSE,
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AuthenticationException extends Exception {
|
public static class AuthenticationException extends Exception {
|
||||||
public AuthenticationException(final String message) {
|
public AuthenticationException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthenticationException(final Exception inner) {
|
public AuthenticationException(final Exception inner) {
|
||||||
super(inner);
|
super(inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthenticationException(final String message, final Exception exception) {
|
public AuthenticationException(final String message, final Exception exception) {
|
||||||
super(message,exception);
|
super(message, exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class InvalidStateException extends AuthenticationException {
|
public static class InvalidStateException extends AuthenticationException {
|
||||||
public InvalidStateException(final String message) {
|
public InvalidStateException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public InvalidStateException(final State state) {
|
public InvalidStateException(final State state) {
|
||||||
this("Invalid state: " + state.toString());
|
this("Invalid state: " + state.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||||
this.tagWriter = tagWriter;
|
this.tagWriter = tagWriter;
|
||||||
this.account = account;
|
this.account = account;
|
||||||
this.rng = rng;
|
this.rng = rng;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another
|
* The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another
|
||||||
* mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade
|
* mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade
|
||||||
* attacks).
|
* attacks).
|
||||||
* @return An arbitrary int representing the priority
|
*
|
||||||
*/
|
* @return An arbitrary int representing the priority
|
||||||
public abstract int getPriority();
|
*/
|
||||||
|
public abstract int getPriority();
|
||||||
|
|
||||||
public abstract String getMechanism();
|
public abstract String getMechanism();
|
||||||
public String getClientFirstMessage() {
|
|
||||||
return "";
|
public String getClientFirstMessage() {
|
||||||
}
|
return "";
|
||||||
public String getResponse(final String challenge) throws AuthenticationException {
|
}
|
||||||
return "";
|
|
||||||
}
|
public String getResponse(final String challenge) throws AuthenticationException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +1,74 @@
|
||||||
package eu.siacs.conversations.crypto.sasl;
|
package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.LruCache;
|
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.common.cache.CacheBuilder;
|
||||||
|
|
||||||
import org.bouncycastle.crypto.Digest;
|
import org.bouncycastle.crypto.Digest;
|
||||||
import org.bouncycastle.crypto.macs.HMac;
|
import org.bouncycastle.crypto.macs.HMac;
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.xml.TagWriter;
|
import eu.siacs.conversations.xml.TagWriter;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
|
|
||||||
abstract class ScramMechanism extends SaslMechanism {
|
abstract class ScramMechanism extends SaslMechanism {
|
||||||
// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
|
// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
|
||||||
private final static String GS2_HEADER = "n,,";
|
private final static String GS2_HEADER = "n,,";
|
||||||
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
|
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
|
||||||
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
|
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
|
||||||
private static final LruCache<String, KeyPair> CACHE;
|
|
||||||
static HMac HMAC;
|
|
||||||
static Digest DIGEST;
|
|
||||||
|
|
||||||
static {
|
protected abstract HMac getHMAC();
|
||||||
CACHE = new LruCache<String, KeyPair>(10) {
|
|
||||||
protected KeyPair create(final String k) {
|
|
||||||
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism".
|
|
||||||
// Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()'
|
|
||||||
// is applied to prevent commas in the strings breaking things.
|
|
||||||
final String[] kParts = k.split(",", 5);
|
|
||||||
try {
|
|
||||||
final byte[] saltedPassword, serverKey, clientKey;
|
|
||||||
saltedPassword = hi(CryptoHelper.hexToString(kParts[1]).getBytes(),
|
|
||||||
Base64.decode(CryptoHelper.hexToString(kParts[2]), Base64.DEFAULT), Integer.parseInt(kParts[3]));
|
|
||||||
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
|
|
||||||
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
|
|
||||||
|
|
||||||
return new KeyPair(clientKey, serverKey);
|
protected abstract Digest getDigest();
|
||||||
} catch (final InvalidKeyException | NumberFormatException e) {
|
|
||||||
return null;
|
private static final Cache<CacheKey, KeyPair> CACHE = CacheBuilder.newBuilder().maximumSize(10).build();
|
||||||
}
|
|
||||||
}
|
private static class CacheKey {
|
||||||
};
|
final String algorithm;
|
||||||
|
final String password;
|
||||||
|
final String salt;
|
||||||
|
final int iterations;
|
||||||
|
|
||||||
|
private CacheKey(String algorithm, String password, String salt, int iterations) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.password = password;
|
||||||
|
this.salt = salt;
|
||||||
|
this.iterations = iterations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
CacheKey cacheKey = (CacheKey) o;
|
||||||
|
return iterations == cacheKey.iterations &&
|
||||||
|
Objects.equal(algorithm, cacheKey.algorithm) &&
|
||||||
|
Objects.equal(password, cacheKey.password) &&
|
||||||
|
Objects.equal(salt, cacheKey.salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(algorithm, password, salt, iterations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException {
|
||||||
|
return CACHE.get(new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), () -> {
|
||||||
|
final byte[] saltedPassword, serverKey, clientKey;
|
||||||
|
saltedPassword = hi(password.getBytes(), Base64.decode(salt, Base64.DEFAULT), iterations);
|
||||||
|
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
|
||||||
|
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
|
||||||
|
return new KeyPair(clientKey, serverKey);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String clientNonce;
|
private final String clientNonce;
|
||||||
|
@ -63,20 +84,21 @@ abstract class ScramMechanism extends SaslMechanism {
|
||||||
clientFirstMessageBare = "";
|
clientFirstMessageBare = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static synchronized byte[] hmac(final byte[] key, final byte[] input)
|
private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException {
|
||||||
throws InvalidKeyException {
|
final HMac hMac = getHMAC();
|
||||||
HMAC.init(new KeyParameter(key));
|
hMac.init(new KeyParameter(key));
|
||||||
HMAC.update(input, 0, input.length);
|
hMac.update(input, 0, input.length);
|
||||||
final byte[] out = new byte[HMAC.getMacSize()];
|
final byte[] out = new byte[hMac.getMacSize()];
|
||||||
HMAC.doFinal(out, 0);
|
hMac.doFinal(out, 0);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized byte[] digest(byte[] bytes) {
|
public byte[] digest(byte[] bytes) {
|
||||||
DIGEST.reset();
|
final Digest digest = getDigest();
|
||||||
DIGEST.update(bytes, 0, bytes.length);
|
digest.reset();
|
||||||
final byte[] out = new byte[DIGEST.getDigestSize()];
|
digest.update(bytes, 0, bytes.length);
|
||||||
DIGEST.doFinal(out, 0);
|
final byte[] out = new byte[digest.getDigestSize()];
|
||||||
|
digest.doFinal(out, 0);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +107,7 @@ abstract class ScramMechanism extends SaslMechanism {
|
||||||
* pseudorandom function (PRF) and with dkLen == output length of
|
* pseudorandom function (PRF) and with dkLen == output length of
|
||||||
* HMAC() == output length of H().
|
* HMAC() == output length of H().
|
||||||
*/
|
*/
|
||||||
private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations)
|
private byte[] hi(final byte[] key, final byte[] salt, final int iterations)
|
||||||
throws InvalidKeyException {
|
throws InvalidKeyException {
|
||||||
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
|
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
|
||||||
byte[] out = u.clone();
|
byte[] out = u.clone();
|
||||||
|
@ -171,15 +193,10 @@ abstract class ScramMechanism extends SaslMechanism {
|
||||||
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
|
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
|
||||||
+ clientFinalMessageWithoutProof).getBytes();
|
+ clientFinalMessageWithoutProof).getBytes();
|
||||||
|
|
||||||
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism".
|
final KeyPair keys;
|
||||||
final KeyPair keys = CACHE.get(
|
try {
|
||||||
CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getJid().asBareJid().toEscapedString()).getBytes()) + ","
|
keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount);
|
||||||
+ CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getPassword()).getBytes()) + ","
|
} catch (ExecutionException e) {
|
||||||
+ CryptoHelper.bytesToHex(salt.getBytes()) + ","
|
|
||||||
+ iterationCount + ","
|
|
||||||
+ getMechanism()
|
|
||||||
);
|
|
||||||
if (keys == null) {
|
|
||||||
throw new AuthenticationException("Invalid keys generated");
|
throw new AuthenticationException("Invalid keys generated");
|
||||||
}
|
}
|
||||||
final byte[] clientSignature;
|
final byte[] clientSignature;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.siacs.conversations.crypto.sasl;
|
package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.Digest;
|
||||||
import org.bouncycastle.crypto.digests.SHA1Digest;
|
import org.bouncycastle.crypto.digests.SHA1Digest;
|
||||||
import org.bouncycastle.crypto.macs.HMac;
|
import org.bouncycastle.crypto.macs.HMac;
|
||||||
|
|
||||||
|
@ -9,22 +10,30 @@ import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.xml.TagWriter;
|
import eu.siacs.conversations.xml.TagWriter;
|
||||||
|
|
||||||
public class ScramSha1 extends ScramMechanism {
|
public class ScramSha1 extends ScramMechanism {
|
||||||
static {
|
|
||||||
DIGEST = new SHA1Digest();
|
|
||||||
HMAC = new HMac(new SHA1Digest());
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
public static final String MECHANISM = "SCRAM-SHA-1";
|
||||||
super(tagWriter, account, rng);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getPriority() {
|
protected HMac getHMAC() {
|
||||||
return 20;
|
return new HMac(new SHA1Digest());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMechanism() {
|
protected Digest getDigest() {
|
||||||
return "SCRAM-SHA-1";
|
return new SHA1Digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||||
|
super(tagWriter, account, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.siacs.conversations.crypto.sasl;
|
package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.Digest;
|
||||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||||
import org.bouncycastle.crypto.macs.HMac;
|
import org.bouncycastle.crypto.macs.HMac;
|
||||||
|
|
||||||
|
@ -9,22 +10,30 @@ import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.xml.TagWriter;
|
import eu.siacs.conversations.xml.TagWriter;
|
||||||
|
|
||||||
public class ScramSha256 extends ScramMechanism {
|
public class ScramSha256 extends ScramMechanism {
|
||||||
static {
|
|
||||||
DIGEST = new SHA256Digest();
|
|
||||||
HMAC = new HMac(new SHA256Digest());
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
public static final String MECHANISM = "SCRAM-SHA-256";
|
||||||
super(tagWriter, account, rng);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getPriority() {
|
protected HMac getHMAC() {
|
||||||
return 25;
|
return new HMac(new SHA256Digest());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getMechanism() {
|
protected Digest getDigest() {
|
||||||
return "SCRAM-SHA-256";
|
return new SHA256Digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||||
|
super(tagWriter, account, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.Digest;
|
||||||
|
import org.bouncycastle.crypto.digests.SHA512Digest;
|
||||||
|
import org.bouncycastle.crypto.macs.HMac;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
import eu.siacs.conversations.xml.TagWriter;
|
||||||
|
|
||||||
|
public class ScramSha512 extends ScramMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "SCRAM-SHA-512";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HMac getHMAC() {
|
||||||
|
return new HMac(new SHA512Digest());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Digest getDigest() {
|
||||||
|
return new SHA512Digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScramSha512(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||||
|
super(tagWriter, account, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,69 +10,69 @@ import java.util.NoSuchElementException;
|
||||||
* A tokenizer for GS2 header strings
|
* A tokenizer for GS2 header strings
|
||||||
*/
|
*/
|
||||||
public final class Tokenizer implements Iterator<String>, Iterable<String> {
|
public final class Tokenizer implements Iterator<String>, Iterable<String> {
|
||||||
private final List<String> parts;
|
private final List<String> parts;
|
||||||
private int index;
|
private int index;
|
||||||
|
|
||||||
public Tokenizer(final byte[] challenge) {
|
public Tokenizer(final byte[] challenge) {
|
||||||
final String challengeString = new String(challenge);
|
final String challengeString = new String(challenge);
|
||||||
parts = new ArrayList<>(Arrays.asList(challengeString.split(",")));
|
parts = new ArrayList<>(Arrays.asList(challengeString.split(",")));
|
||||||
// Trim parts.
|
// Trim parts.
|
||||||
for (int i = 0; i < parts.size(); i++) {
|
for (int i = 0; i < parts.size(); i++) {
|
||||||
parts.set(i, parts.get(i).trim());
|
parts.set(i, parts.get(i).trim());
|
||||||
}
|
}
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if there is at least one more element, false otherwise.
|
* Returns true if there is at least one more element, false otherwise.
|
||||||
*
|
*
|
||||||
* @see #next
|
* @see #next
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean hasNext() {
|
public boolean hasNext() {
|
||||||
return parts.size() != index + 1;
|
return parts.size() != index + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the next object and advances the iterator.
|
* Returns the next object and advances the iterator.
|
||||||
*
|
*
|
||||||
* @return the next object.
|
* @return the next object.
|
||||||
* @throws java.util.NoSuchElementException if there are no more elements.
|
* @throws java.util.NoSuchElementException if there are no more elements.
|
||||||
* @see #hasNext
|
* @see #hasNext
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String next() {
|
public String next() {
|
||||||
if (hasNext()) {
|
if (hasNext()) {
|
||||||
return parts.get(index++);
|
return parts.get(index++);
|
||||||
} else {
|
} else {
|
||||||
throw new NoSuchElementException("No such element. Size is: " + parts.size());
|
throw new NoSuchElementException("No such element. Size is: " + parts.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the last object returned by {@code next} from the collection.
|
* Removes the last object returned by {@code next} from the collection.
|
||||||
* This method can only be called once between each call to {@code next}.
|
* This method can only be called once between each call to {@code next}.
|
||||||
*
|
*
|
||||||
* @throws UnsupportedOperationException if removing is not supported by the collection being
|
* @throws UnsupportedOperationException if removing is not supported by the collection being
|
||||||
* iterated.
|
* iterated.
|
||||||
* @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
|
* @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
|
||||||
* already been called after the last call to {@code next}.
|
* already been called after the last call to {@code next}.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void remove() {
|
public void remove() {
|
||||||
if(index <= 0) {
|
if (index <= 0) {
|
||||||
throw new IllegalStateException("You can't delete an element before first next() method call");
|
throw new IllegalStateException("You can't delete an element before first next() method call");
|
||||||
}
|
}
|
||||||
parts.remove(--index);
|
parts.remove(--index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an {@link java.util.Iterator} for the elements in this object.
|
* Returns an {@link java.util.Iterator} for the elements in this object.
|
||||||
*
|
*
|
||||||
* @return An {@code Iterator} instance.
|
* @return An {@code Iterator} instance.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Iterator<String> iterator() {
|
public Iterator<String> iterator() {
|
||||||
return parts.iterator();
|
return parts.iterator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,8 +158,11 @@ public class MucOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean allowInvites() {
|
public boolean allowInvites() {
|
||||||
final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites");
|
final Field allowInvitesField = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites");
|
||||||
return field != null && "1".equals(field.getValue());
|
final boolean allowInvites = allowInvitesField != null && "1".equals(allowInvitesField.getValue());
|
||||||
|
final Field allowMemberInvitesField = getRoomInfoForm().getFieldByName("muc#roomconfig_allowmemberinvites");
|
||||||
|
final boolean allowMemberInvites = allowMemberInvitesField != null && "1".equals(allowMemberInvitesField.getValue());
|
||||||
|
return allowInvites || allowMemberInvites;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean canChangeSubject() {
|
public boolean canChangeSubject() {
|
||||||
|
|
|
@ -77,7 +77,7 @@ public class FileBackend {
|
||||||
|
|
||||||
private static final String FILE_PROVIDER = ".files";
|
private static final String FILE_PROVIDER = ".files";
|
||||||
private static final float IGNORE_PADDING = 0.15f;
|
private static final float IGNORE_PADDING = 0.15f;
|
||||||
private XmppConnectionService mXmppConnectionService;
|
private final XmppConnectionService mXmppConnectionService;
|
||||||
|
|
||||||
public FileBackend(XmppConnectionService service) {
|
public FileBackend(XmppConnectionService service) {
|
||||||
this.mXmppConnectionService = service;
|
this.mXmppConnectionService = service;
|
||||||
|
|
|
@ -2,6 +2,8 @@ package eu.siacs.conversations.services;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -615,6 +617,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
|
@ -182,7 +182,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
private Toast messageLoaderToast;
|
private Toast messageLoaderToast;
|
||||||
private ConversationsActivity activity;
|
private ConversationsActivity activity;
|
||||||
private boolean reInitRequiredOnStart = true;
|
private boolean reInitRequiredOnStart = true;
|
||||||
private OnClickListener clickToMuc = new OnClickListener() {
|
private final OnClickListener clickToMuc = new OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
@ -192,14 +192,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private OnClickListener leaveMuc = new OnClickListener() {
|
private final OnClickListener leaveMuc = new OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
activity.xmppConnectionService.archiveConversation(conversation);
|
activity.xmppConnectionService.archiveConversation(conversation);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private OnClickListener joinMuc = new OnClickListener() {
|
private final OnClickListener joinMuc = new OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
@ -207,7 +207,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private OnClickListener acceptJoin = new OnClickListener() {
|
private final OnClickListener acceptJoin = new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
conversation.setAttribute("accept_non_anonymous", true);
|
conversation.setAttribute("accept_non_anonymous", true);
|
||||||
|
@ -216,7 +216,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private OnClickListener enterPassword = new OnClickListener() {
|
private final OnClickListener enterPassword = new OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
@ -231,7 +231,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private OnScrollListener mOnScrollListener = new OnScrollListener() {
|
private final OnScrollListener mOnScrollListener = new OnScrollListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScrollStateChanged(AbsListView view, int scrollState) {
|
public void onScrollStateChanged(AbsListView view, int scrollState) {
|
||||||
|
@ -310,7 +310,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
|
private final EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
|
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
|
||||||
// try to get permission to read the image, if applicable
|
// try to get permission to read the image, if applicable
|
||||||
|
@ -333,7 +333,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private Message selectedMessage;
|
private Message selectedMessage;
|
||||||
private OnClickListener mEnableAccountListener = new OnClickListener() {
|
private final OnClickListener mEnableAccountListener = new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
final Account account = conversation == null ? null : conversation.getAccount();
|
final Account account = conversation == null ? null : conversation.getAccount();
|
||||||
|
@ -343,7 +343,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private OnClickListener mUnblockClickListener = new OnClickListener() {
|
private final OnClickListener mUnblockClickListener = new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(final View v) {
|
public void onClick(final View v) {
|
||||||
v.post(() -> v.setVisibility(View.INVISIBLE));
|
v.post(() -> v.setVisibility(View.INVISIBLE));
|
||||||
|
@ -354,8 +354,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private OnClickListener mBlockClickListener = this::showBlockSubmenu;
|
private final OnClickListener mBlockClickListener = this::showBlockSubmenu;
|
||||||
private OnClickListener mAddBackClickListener = new OnClickListener() {
|
private final OnClickListener mAddBackClickListener = new OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
@ -366,8 +366,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu;
|
private final View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu;
|
||||||
private OnClickListener mAllowPresenceSubscription = new OnClickListener() {
|
private final OnClickListener mAllowPresenceSubscription = new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
final Contact contact = conversation == null ? null : conversation.getContact();
|
final Contact contact = conversation == null ? null : conversation.getContact();
|
||||||
|
@ -400,8 +400,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
updateSnackBar(conversation);
|
updateSnackBar(conversation);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
|
private final AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
|
||||||
private OnEditorActionListener mEditorActionListener = (v, actionId, event) -> {
|
private final OnEditorActionListener mEditorActionListener = (v, actionId, event) -> {
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
if (imm != null && imm.isFullscreenMode()) {
|
if (imm != null && imm.isFullscreenMode()) {
|
||||||
|
@ -413,7 +413,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private OnClickListener mScrollButtonListener = new OnClickListener() {
|
private final OnClickListener mScrollButtonListener = new OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
@ -421,7 +421,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
setSelection(binding.messagesView.getCount() - 1, true);
|
setSelection(binding.messagesView.getCount() - 1, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private OnClickListener mSendButtonListener = new OnClickListener() {
|
private final OnClickListener mSendButtonListener = new OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
@ -517,7 +517,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
|
|
||||||
private static Conversation getConversation(Activity activity, @IdRes int res) {
|
private static Conversation getConversation(Activity activity, @IdRes int res) {
|
||||||
final Fragment fragment = activity.getFragmentManager().findFragmentById(res);
|
final Fragment fragment = activity.getFragmentManager().findFragmentById(res);
|
||||||
if (fragment != null && fragment instanceof ConversationFragment) {
|
if (fragment instanceof ConversationFragment) {
|
||||||
return ((ConversationFragment) fragment).getConversation();
|
return ((ConversationFragment) fragment).getConversation();
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
@ -527,11 +527,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
public static ConversationFragment get(Activity activity) {
|
public static ConversationFragment get(Activity activity) {
|
||||||
FragmentManager fragmentManager = activity.getFragmentManager();
|
FragmentManager fragmentManager = activity.getFragmentManager();
|
||||||
Fragment fragment = fragmentManager.findFragmentById(R.id.main_fragment);
|
Fragment fragment = fragmentManager.findFragmentById(R.id.main_fragment);
|
||||||
if (fragment != null && fragment instanceof ConversationFragment) {
|
if (fragment instanceof ConversationFragment) {
|
||||||
return (ConversationFragment) fragment;
|
return (ConversationFragment) fragment;
|
||||||
} else {
|
} else {
|
||||||
fragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
|
fragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
|
||||||
return fragment != null && fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null;
|
return fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -986,7 +986,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
menuCall.setVisible(false);
|
menuCall.setVisible(false);
|
||||||
menuOngoingCall.setVisible(false);
|
menuOngoingCall.setVisible(false);
|
||||||
} else {
|
} else {
|
||||||
final Optional<OngoingRtpSession> ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact());
|
final XmppConnectionService service = activity.xmppConnectionService;
|
||||||
|
final Optional<OngoingRtpSession> ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact());
|
||||||
if (ongoingRtpSession.isPresent()) {
|
if (ongoingRtpSession.isPresent()) {
|
||||||
menuOngoingCall.setVisible(true);
|
menuOngoingCall.setVisible(true);
|
||||||
menuCall.setVisible(false);
|
menuCall.setVisible(false);
|
||||||
|
@ -998,7 +999,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
}
|
}
|
||||||
menuContactDetails.setVisible(!this.conversation.withSelf());
|
menuContactDetails.setVisible(!this.conversation.withSelf());
|
||||||
menuMucDetails.setVisible(false);
|
menuMucDetails.setVisible(false);
|
||||||
final XmppConnectionService service = activity.xmppConnectionService;
|
|
||||||
menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null);
|
menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null);
|
||||||
}
|
}
|
||||||
if (conversation.isMuted()) {
|
if (conversation.isMuted()) {
|
||||||
|
|
|
@ -48,6 +48,7 @@ import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
|
|
||||||
|
@ -371,7 +372,10 @@ public class ConversationsOverviewFragment extends XmppFragment {
|
||||||
|
|
||||||
private void selectAccountToStartEasyInvite() {
|
private void selectAccountToStartEasyInvite() {
|
||||||
final List<Account> accounts = EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService);
|
final List<Account> accounts = EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService);
|
||||||
if (accounts.size() == 1) {
|
if (accounts.size() == 0) {
|
||||||
|
//This can technically happen if opening the menu item races with accounts reconnecting or something
|
||||||
|
Toast.makeText(getActivity(),R.string.no_active_accounts_support_this, Toast.LENGTH_LONG).show();
|
||||||
|
} else if (accounts.size() == 1) {
|
||||||
openEasyInviteScreen(accounts.get(0));
|
openEasyInviteScreen(accounts.get(0));
|
||||||
} else {
|
} else {
|
||||||
final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));
|
final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));
|
||||||
|
|
|
@ -39,6 +39,7 @@ import android.text.TextWatcher;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
@ -130,7 +131,8 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
public void onCreateContextMenu(final ContextMenu menu, final View v, ContextMenu.ContextMenuInfo menuInfo) {
|
||||||
|
v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
|
||||||
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
|
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||||
final Message message = this.messages.get(acmi.position);
|
final Message message = this.messages.get(acmi.position);
|
||||||
this.selectedMessageReference = new WeakReference<>(message);
|
this.selectedMessageReference = new WeakReference<>(message);
|
||||||
|
|
|
@ -368,12 +368,13 @@ public abstract class XmppActivity extends ActionBarActivity {
|
||||||
|
|
||||||
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
|
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
|
||||||
final Contact contact = conversation.getContact();
|
final Contact contact = conversation.getContact();
|
||||||
if (!contact.showInRoster()) {
|
if (contact.showInRoster() || contact.isSelf()) {
|
||||||
showAddToRosterDialog(conversation.getContact());
|
|
||||||
} else {
|
|
||||||
final Presences presences = contact.getPresences();
|
final Presences presences = contact.getPresences();
|
||||||
if (presences.size() == 0) {
|
if (presences.size() == 0) {
|
||||||
if (!contact.getOption(Contact.Options.TO)
|
if (contact.isSelf()) {
|
||||||
|
conversation.setNextCounterpart(null);
|
||||||
|
listener.onPresenceSelected();
|
||||||
|
} else if (!contact.getOption(Contact.Options.TO)
|
||||||
&& !contact.getOption(Contact.Options.ASKING)
|
&& !contact.getOption(Contact.Options.ASKING)
|
||||||
&& contact.getAccount().getStatus() == Account.State.ONLINE) {
|
&& contact.getAccount().getStatus() == Account.State.ONLINE) {
|
||||||
showAskForPresenceDialog(contact);
|
showAskForPresenceDialog(contact);
|
||||||
|
@ -391,6 +392,8 @@ public abstract class XmppActivity extends ActionBarActivity {
|
||||||
} else {
|
} else {
|
||||||
PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
|
PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
showAddToRosterDialog(conversation.getContact());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ public class SSLSocketHelper {
|
||||||
final Collection<String> supportedProtocols = new LinkedList<>(
|
final Collection<String> supportedProtocols = new LinkedList<>(
|
||||||
Arrays.asList(sslSocket.getSupportedProtocols()));
|
Arrays.asList(sslSocket.getSupportedProtocols()));
|
||||||
supportedProtocols.remove("SSLv3");
|
supportedProtocols.remove("SSLv3");
|
||||||
supportProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]);
|
supportProtocols = supportedProtocols.toArray(new String[0]);
|
||||||
|
|
||||||
sslSocket.setEnabledProtocols(supportProtocols);
|
sslSocket.setEnabledProtocols(supportProtocols);
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ import eu.siacs.conversations.crypto.sasl.Plain;
|
||||||
import eu.siacs.conversations.crypto.sasl.SaslMechanism;
|
import eu.siacs.conversations.crypto.sasl.SaslMechanism;
|
||||||
import eu.siacs.conversations.crypto.sasl.ScramSha1;
|
import eu.siacs.conversations.crypto.sasl.ScramSha1;
|
||||||
import eu.siacs.conversations.crypto.sasl.ScramSha256;
|
import eu.siacs.conversations.crypto.sasl.ScramSha256;
|
||||||
|
import eu.siacs.conversations.crypto.sasl.ScramSha512;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
||||||
|
@ -106,32 +107,29 @@ public class XmppConnection implements Runnable {
|
||||||
private static final int PACKET_IQ = 0;
|
private static final int PACKET_IQ = 0;
|
||||||
private static final int PACKET_MESSAGE = 1;
|
private static final int PACKET_MESSAGE = 1;
|
||||||
private static final int PACKET_PRESENCE = 2;
|
private static final int PACKET_PRESENCE = 2;
|
||||||
public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() {
|
public final OnIqPacketReceived registrationResponseListener = (account, packet) -> {
|
||||||
@Override
|
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||||
public void onIqPacketReceived(Account account, IqPacket packet) {
|
account.setOption(Account.OPTION_REGISTER, false);
|
||||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully registered new account on server");
|
||||||
account.setOption(Account.OPTION_REGISTER, false);
|
throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully registered new account on server");
|
} else {
|
||||||
throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
|
final List<String> PASSWORD_TOO_WEAK_MSGS = Arrays.asList(
|
||||||
} else {
|
"The password is too weak",
|
||||||
final List<String> PASSWORD_TOO_WEAK_MSGS = Arrays.asList(
|
"Please use a longer password.");
|
||||||
"The password is too weak",
|
Element error = packet.findChild("error");
|
||||||
"Please use a longer password.");
|
Account.State state = Account.State.REGISTRATION_FAILED;
|
||||||
Element error = packet.findChild("error");
|
if (error != null) {
|
||||||
Account.State state = Account.State.REGISTRATION_FAILED;
|
if (error.hasChild("conflict")) {
|
||||||
if (error != null) {
|
state = Account.State.REGISTRATION_CONFLICT;
|
||||||
if (error.hasChild("conflict")) {
|
} else if (error.hasChild("resource-constraint")
|
||||||
state = Account.State.REGISTRATION_CONFLICT;
|
&& "wait".equals(error.getAttribute("type"))) {
|
||||||
} else if (error.hasChild("resource-constraint")
|
state = Account.State.REGISTRATION_PLEASE_WAIT;
|
||||||
&& "wait".equals(error.getAttribute("type"))) {
|
} else if (error.hasChild("not-acceptable")
|
||||||
state = Account.State.REGISTRATION_PLEASE_WAIT;
|
&& PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) {
|
||||||
} else if (error.hasChild("not-acceptable")
|
state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
|
||||||
&& PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) {
|
|
||||||
state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw new StateChangingError(state);
|
|
||||||
}
|
}
|
||||||
|
throw new StateChangingError(state);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
protected final Account account;
|
protected final Account account;
|
||||||
|
@ -159,10 +157,10 @@ public class XmppConnection implements Runnable {
|
||||||
private long lastSessionStarted = 0;
|
private long lastSessionStarted = 0;
|
||||||
private long lastDiscoStarted = 0;
|
private long lastDiscoStarted = 0;
|
||||||
private boolean isMamPreferenceAlways = false;
|
private boolean isMamPreferenceAlways = false;
|
||||||
private AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
|
private final AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
|
||||||
private AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
|
private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
|
||||||
private AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
|
private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
|
||||||
private AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
|
private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
|
||||||
private boolean mInteractive = false;
|
private boolean mInteractive = false;
|
||||||
private int attempt = 0;
|
private int attempt = 0;
|
||||||
private OnPresencePacketReceived presenceListener = null;
|
private OnPresencePacketReceived presenceListener = null;
|
||||||
|
@ -744,7 +742,7 @@ public class XmppConnection implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException {
|
private void processMessage(final Tag currentTag) throws IOException {
|
||||||
final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
|
final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
|
||||||
if (!packet.valid()) {
|
if (!packet.valid()) {
|
||||||
Log.e(Config.LOGTAG, "encountered invalid message from='" + packet.getFrom() + "' to='" + packet.getTo() + "'");
|
Log.e(Config.LOGTAG, "encountered invalid message from='" + packet.getFrom() + "' to='" + packet.getTo() + "'");
|
||||||
|
@ -753,7 +751,7 @@ public class XmppConnection implements Runnable {
|
||||||
this.messageListener.onMessagePacketReceived(account, packet);
|
this.messageListener.onMessagePacketReceived(account, packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException {
|
private void processPresence(final Tag currentTag) throws IOException {
|
||||||
PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
|
PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
|
||||||
if (!packet.valid()) {
|
if (!packet.valid()) {
|
||||||
Log.e(Config.LOGTAG, "encountered invalid presence from='" + packet.getFrom() + "' to='" + packet.getTo() + "'");
|
Log.e(Config.LOGTAG, "encountered invalid presence from='" + packet.getFrom() + "' to='" + packet.getTo() + "'");
|
||||||
|
@ -807,7 +805,7 @@ public class XmppConnection implements Runnable {
|
||||||
return sslSocket;
|
return sslSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processStreamFeatures(final Tag currentTag) throws XmlPullParserException, IOException {
|
private void processStreamFeatures(final Tag currentTag) throws IOException {
|
||||||
this.streamFeatures = tagReader.readElement(currentTag);
|
this.streamFeatures = tagReader.readElement(currentTag);
|
||||||
final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
|
final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
|
||||||
final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
|
final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
|
||||||
|
@ -843,20 +841,21 @@ public class XmppConnection implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void authenticate() throws IOException {
|
private void authenticate() throws IOException {
|
||||||
final List<String> mechanisms = extractMechanisms(streamFeatures
|
final List<String> mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms"));
|
||||||
.findChild("mechanisms"));
|
|
||||||
final Element auth = new Element("auth", Namespace.SASL);
|
final Element auth = new Element("auth", Namespace.SASL);
|
||||||
if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
|
if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) {
|
||||||
saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
|
saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
|
||||||
} else if (mechanisms.contains("SCRAM-SHA-256")) {
|
} else if (mechanisms.contains(ScramSha512.MECHANISM)) {
|
||||||
|
saslMechanism = new ScramSha512(tagWriter, account, mXmppConnectionService.getRNG());
|
||||||
|
} else if (mechanisms.contains(ScramSha256.MECHANISM)) {
|
||||||
saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
|
saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
|
||||||
} else if (mechanisms.contains("SCRAM-SHA-1")) {
|
} else if (mechanisms.contains(ScramSha1.MECHANISM)) {
|
||||||
saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
|
saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
|
||||||
} else if (mechanisms.contains("PLAIN") && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) {
|
} else if (mechanisms.contains(Plain.MECHANISM) && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) {
|
||||||
saslMechanism = new Plain(tagWriter, account);
|
saslMechanism = new Plain(tagWriter, account);
|
||||||
} else if (mechanisms.contains("DIGEST-MD5")) {
|
} else if (mechanisms.contains(DigestMd5.MECHANISM)) {
|
||||||
saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
|
saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
|
||||||
} else if (mechanisms.contains("ANONYMOUS")) {
|
} else if (mechanisms.contains(Anonymous.MECHANISM)) {
|
||||||
saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG());
|
saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG());
|
||||||
}
|
}
|
||||||
if (saslMechanism != null) {
|
if (saslMechanism != null) {
|
||||||
|
@ -1238,27 +1237,27 @@ public class XmppConnection implements Runnable {
|
||||||
request.setTo(account.getDomain());
|
request.setTo(account.getDomain());
|
||||||
request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
|
request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
|
||||||
sendIqPacket(request, (account, response) -> {
|
sendIqPacket(request, (account, response) -> {
|
||||||
if (response.getType() == IqPacket.TYPE.RESULT) {
|
if (response.getType() == IqPacket.TYPE.RESULT) {
|
||||||
final Element query = response.findChild("query",Namespace.DISCO_ITEMS);
|
final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
|
||||||
if (query == null) {
|
if (query == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final HashMap<String, Jid> commands = new HashMap<>();
|
final HashMap<String, Jid> commands = new HashMap<>();
|
||||||
for(final Element child : query.getChildren()) {
|
for (final Element child : query.getChildren()) {
|
||||||
if ("item".equals(child.getName())) {
|
if ("item".equals(child.getName())) {
|
||||||
final String node = child.getAttribute("node");
|
final String node = child.getAttribute("node");
|
||||||
final Jid jid = child.getAttributeAsJid("jid");
|
final Jid jid = child.getAttributeAsJid("jid");
|
||||||
if (node != null && jid != null) {
|
if (node != null && jid != null) {
|
||||||
commands.put(node, jid);
|
commands.put(node, jid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG,commands.toString());
|
Log.d(Config.LOGTAG, commands.toString());
|
||||||
synchronized (this.commands) {
|
synchronized (this.commands) {
|
||||||
this.commands.clear();
|
this.commands.clear();
|
||||||
this.commands.putAll(commands);
|
this.commands.putAll(commands);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1297,7 +1296,7 @@ public class XmppConnection implements Runnable {
|
||||||
iq.query("http://jabber.org/protocol/disco#items");
|
iq.query("http://jabber.org/protocol/disco#items");
|
||||||
this.sendIqPacket(iq, (account, packet) -> {
|
this.sendIqPacket(iq, (account, packet) -> {
|
||||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||||
HashSet<Jid> items = new HashSet<Jid>();
|
final HashSet<Jid> items = new HashSet<>();
|
||||||
final List<Element> elements = packet.query().getChildren();
|
final List<Element> elements = packet.query().getChildren();
|
||||||
for (final Element element : elements) {
|
for (final Element element : elements) {
|
||||||
if (element.getName().equals("item")) {
|
if (element.getName().equals("item")) {
|
||||||
|
@ -1325,23 +1324,19 @@ public class XmppConnection implements Runnable {
|
||||||
private void sendEnableCarbons() {
|
private void sendEnableCarbons() {
|
||||||
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
|
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
|
||||||
iq.addChild("enable", "urn:xmpp:carbons:2");
|
iq.addChild("enable", "urn:xmpp:carbons:2");
|
||||||
this.sendIqPacket(iq, new OnIqPacketReceived() {
|
this.sendIqPacket(iq, (account, packet) -> {
|
||||||
|
if (!packet.hasChild("error")) {
|
||||||
@Override
|
Log.d(Config.LOGTAG, account.getJid().asBareJid()
|
||||||
public void onIqPacketReceived(final Account account, final IqPacket packet) {
|
+ ": successfully enabled carbons");
|
||||||
if (!packet.hasChild("error")) {
|
features.carbonsEnabled = true;
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid()
|
} else {
|
||||||
+ ": successfully enabled carbons");
|
Log.d(Config.LOGTAG, account.getJid().asBareJid()
|
||||||
features.carbonsEnabled = true;
|
+ ": error enableing carbons " + packet.toString());
|
||||||
} else {
|
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid()
|
|
||||||
+ ": error enableing carbons " + packet.toString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processStreamError(final Tag currentTag) throws XmlPullParserException, IOException {
|
private void processStreamError(final Tag currentTag) throws IOException {
|
||||||
final Element streamError = tagReader.readElement(currentTag);
|
final Element streamError = tagReader.readElement(currentTag);
|
||||||
if (streamError == null) {
|
if (streamError == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -1594,8 +1589,8 @@ public class XmppConnection implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getMucServersWithholdAccount() {
|
public List<String> getMucServersWithholdAccount() {
|
||||||
List<String> servers = getMucServers();
|
final List<String> servers = getMucServers();
|
||||||
servers.remove(account.getDomain());
|
servers.remove(account.getDomain().toEscapedString());
|
||||||
return servers;
|
return servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1766,7 +1761,7 @@ public class XmppConnection implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class StateChangingError extends Error {
|
private static class StateChangingError extends Error {
|
||||||
private final Account.State state;
|
private final Account.State state;
|
||||||
|
|
||||||
public StateChangingError(Account.State state) {
|
public StateChangingError(Account.State state) {
|
||||||
|
@ -1774,7 +1769,7 @@ public class XmppConnection implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class StateChangingException extends IOException {
|
private static class StateChangingException extends IOException {
|
||||||
private final Account.State state;
|
private final Account.State state;
|
||||||
|
|
||||||
public StateChangingException(Account.State state) {
|
public StateChangingException(Account.State state) {
|
||||||
|
|
|
@ -143,14 +143,16 @@ public class SessionDescription {
|
||||||
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
|
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
|
||||||
final String ufrag = transport.getAttribute("ufrag");
|
final String ufrag = transport.getAttribute("ufrag");
|
||||||
final String pwd = transport.getAttribute("pwd");
|
final String pwd = transport.getAttribute("pwd");
|
||||||
if (!Strings.isNullOrEmpty(ufrag)) {
|
if (Strings.isNullOrEmpty(ufrag)) {
|
||||||
mediaAttributes.put("ice-ufrag", ufrag);
|
throw new IllegalArgumentException("Transport element is missing required ufrag attribute");
|
||||||
}
|
}
|
||||||
checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
|
checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
|
||||||
if (!Strings.isNullOrEmpty(pwd)) {
|
mediaAttributes.put("ice-ufrag", ufrag);
|
||||||
mediaAttributes.put("ice-pwd", pwd);
|
if (Strings.isNullOrEmpty(pwd)) {
|
||||||
|
throw new IllegalArgumentException("Transport element is missing required pwd attribute");
|
||||||
}
|
}
|
||||||
checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
|
checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
|
||||||
|
mediaAttributes.put("ice-pwd", pwd);
|
||||||
mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS);
|
mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS);
|
||||||
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
|
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
|
||||||
if (fingerprint != null) {
|
if (fingerprint != null) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
@ -93,6 +94,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
||||||
if (pair.length == 2 && "candidate".equals(pair[0])) {
|
if (pair.length == 2 && "candidate".equals(pair[0])) {
|
||||||
final String[] segments = pair[1].split(" ");
|
final String[] segments = pair[1].split(" ");
|
||||||
if (segments.length >= 6) {
|
if (segments.length >= 6) {
|
||||||
|
final String id = UUID.randomUUID().toString();
|
||||||
final String foundation = segments[0];
|
final String foundation = segments[0];
|
||||||
final String component = segments[1];
|
final String component = segments[1];
|
||||||
final String transport = segments[2].toLowerCase(Locale.ROOT);
|
final String transport = segments[2].toLowerCase(Locale.ROOT);
|
||||||
|
@ -109,6 +111,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
||||||
candidate.setAttribute("generation", additional.get("generation"));
|
candidate.setAttribute("generation", additional.get("generation"));
|
||||||
candidate.setAttribute("rel-addr", additional.get("raddr"));
|
candidate.setAttribute("rel-addr", additional.get("raddr"));
|
||||||
candidate.setAttribute("rel-port", additional.get("rport"));
|
candidate.setAttribute("rel-port", additional.get("rport"));
|
||||||
|
candidate.setAttribute("id", id);
|
||||||
candidate.setAttribute("ip", connectionAddress);
|
candidate.setAttribute("ip", connectionAddress);
|
||||||
candidate.setAttribute("port", port);
|
candidate.setAttribute("port", port);
|
||||||
candidate.setAttribute("priority", priority);
|
candidate.setAttribute("priority", priority);
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
<string name="action_add">Nová konverzace</string>
|
<string name="action_add">Nová konverzace</string>
|
||||||
<string name="action_accounts">Nastavení účtů</string>
|
<string name="action_accounts">Nastavení účtů</string>
|
||||||
<string name="action_account">Nastavení účtu</string>
|
<string name="action_account">Nastavení účtu</string>
|
||||||
|
<string name="action_end_conversation">Zavřít konverzaci</string>
|
||||||
<string name="action_contact_details">Detaily kontaktu</string>
|
<string name="action_contact_details">Detaily kontaktu</string>
|
||||||
|
<string name="action_muc_details">Detaily skupinového chatu</string>
|
||||||
<string name="action_add_account">Přidat účet</string>
|
<string name="action_add_account">Přidat účet</string>
|
||||||
<string name="action_edit_contact">Upravit jméno</string>
|
<string name="action_edit_contact">Upravit jméno</string>
|
||||||
<string name="action_add_phone_book">Přidat do adresáře</string>
|
<string name="action_add_phone_book">Přidat do adresáře</string>
|
||||||
|
@ -17,6 +19,7 @@
|
||||||
<string name="title_activity_settings">Nastavení</string>
|
<string name="title_activity_settings">Nastavení</string>
|
||||||
<string name="title_activity_sharewith">Sdílet s konverzací</string>
|
<string name="title_activity_sharewith">Sdílet s konverzací</string>
|
||||||
<string name="title_activity_start_conversation">Začít konverzaci</string>
|
<string name="title_activity_start_conversation">Začít konverzaci</string>
|
||||||
|
<string name="title_activity_choose_contact">Vybrat kontakt</string>
|
||||||
<string name="title_activity_block_list">Seznam blokovaných</string>
|
<string name="title_activity_block_list">Seznam blokovaných</string>
|
||||||
<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>
|
||||||
|
@ -30,14 +33,17 @@
|
||||||
<string name="moderator">Moderátor</string>
|
<string name="moderator">Moderátor</string>
|
||||||
<string name="participant">Účastník</string>
|
<string name="participant">Účastník</string>
|
||||||
<string name="visitor">Návštěvník</string>
|
<string name="visitor">Návštěvník</string>
|
||||||
|
<string name="remove_contact_text">Přejete si odstranit %s ze seznamu kontaktů? Předešlé rozhovory nebudou odstraněny.</string>
|
||||||
<string name="block_contact_text">Chcete zablokovat příjem zpráv od %s?</string>
|
<string name="block_contact_text">Chcete zablokovat příjem zpráv od %s?</string>
|
||||||
<string name="unblock_contact_text">Chcete odblokovat příjem zpráv od %s?</string>
|
<string name="unblock_contact_text">Chcete odblokovat příjem zpráv od %s?</string>
|
||||||
<string name="block_domain_text">Zablokovat všechny kontakty z %s?</string>
|
<string name="block_domain_text">Zablokovat všechny kontakty z %s?</string>
|
||||||
<string name="unblock_domain_text">Odblokovat všechny kontakty z %s?</string>
|
<string name="unblock_domain_text">Odblokovat všechny kontakty z %s?</string>
|
||||||
<string name="contact_blocked">Kontakty zablokovány</string>
|
<string name="contact_blocked">Kontakty zablokovány</string>
|
||||||
|
<string name="remove_bookmark_text">Přejete si odstranit %s ze záložek? Předešlé rozhovory pod záložkou nebudou odstraněny.</string>
|
||||||
<string name="register_account">Registrovat nový účet na serveru</string>
|
<string name="register_account">Registrovat nový účet na serveru</string>
|
||||||
<string name="change_password_on_server">Změnit heslo na serveru</string>
|
<string name="change_password_on_server">Změnit heslo na serveru</string>
|
||||||
<string name="share_with">Sdílet s...</string>
|
<string name="share_with">Sdílet s...</string>
|
||||||
|
<string name="start_conversation">Začít konverzaci</string>
|
||||||
<string name="invite_contact">Pozvat kontakt</string>
|
<string name="invite_contact">Pozvat kontakt</string>
|
||||||
<string name="contacts">Kontakty</string>
|
<string name="contacts">Kontakty</string>
|
||||||
<string name="cancel">Zrušit</string>
|
<string name="cancel">Zrušit</string>
|
||||||
|
@ -144,9 +150,14 @@
|
||||||
<string name="server_info_unavailable">nedostupný</string>
|
<string name="server_info_unavailable">nedostupný</string>
|
||||||
<string name="missing_public_keys">Chybí oznámení o veřejném klíči</string>
|
<string name="missing_public_keys">Chybí oznámení o veřejném klíči</string>
|
||||||
<string name="last_seen_now">právě spatřen</string>
|
<string name="last_seen_now">právě spatřen</string>
|
||||||
|
<string name="last_seen_min">naposledy spatřen před minutou</string>
|
||||||
<string name="last_seen_mins">naposledy spatřen před %d minutami</string>
|
<string name="last_seen_mins">naposledy spatřen před %d minutami</string>
|
||||||
|
<string name="last_seen_hour">naposledy spatřen před hodinou</string>
|
||||||
<string name="last_seen_hours">naposledy spatřen před %d hodinami</string>
|
<string name="last_seen_hours">naposledy spatřen před %d hodinami</string>
|
||||||
|
<string name="last_seen_day">naposledy spatřen včera</string>
|
||||||
<string name="last_seen_days">naposledy spatřen před %d dny</string>
|
<string name="last_seen_days">naposledy spatřen před %d dny</string>
|
||||||
|
<string name="install_openkeychain">Šifrovaná zpráva. Nainstalujte OpenKeychain pro její dešifrování.</string>
|
||||||
|
<string name="openpgp_messages_found">Nalezeny nové OpenPGP šifrované zprávy</string>
|
||||||
<string name="openpgp_key_id">OpenPGP ID klíče</string>
|
<string name="openpgp_key_id">OpenPGP ID klíče</string>
|
||||||
<string name="omemo_fingerprint">OMEMO otisk</string>
|
<string name="omemo_fingerprint">OMEMO otisk</string>
|
||||||
<string name="omemo_fingerprint_x509">v\\OMEMO otisk</string>
|
<string name="omemo_fingerprint_x509">v\\OMEMO otisk</string>
|
||||||
|
@ -160,6 +171,7 @@
|
||||||
<string name="bookmarks">Záložky</string>
|
<string name="bookmarks">Záložky</string>
|
||||||
<string name="search">Hledat</string>
|
<string name="search">Hledat</string>
|
||||||
<string name="enter_contact">Vložit kontakt</string>
|
<string name="enter_contact">Vložit kontakt</string>
|
||||||
|
<string name="delete_contact">Smazat kontakt</string>
|
||||||
<string name="view_contact_details">Zobrazit detaily kontaktu</string>
|
<string name="view_contact_details">Zobrazit detaily kontaktu</string>
|
||||||
<string name="block_contact">Zablokovat kontakt</string>
|
<string name="block_contact">Zablokovat kontakt</string>
|
||||||
<string name="unblock_contact">Odblokovat kontakt</string>
|
<string name="unblock_contact">Odblokovat kontakt</string>
|
||||||
|
|
|
@ -951,4 +951,5 @@
|
||||||
<string name="invite_to_app">Einladung zu Conversations</string>
|
<string name="invite_to_app">Einladung zu Conversations</string>
|
||||||
<string name="unable_to_parse_invite">Einladung kann nicht gelesen werden</string>
|
<string name="unable_to_parse_invite">Einladung kann nicht gelesen werden</string>
|
||||||
<string name="server_does_not_support_easy_onboarding_invites">Server unterstützt keine Generierung von Einladungen</string>
|
<string name="server_does_not_support_easy_onboarding_invites">Server unterstützt keine Generierung von Einladungen</string>
|
||||||
|
<string name="no_active_accounts_support_this">Keine aktiven Konten unterstützen diese Funktion</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -951,4 +951,5 @@
|
||||||
<string name="invite_to_app">Convida a Conversations</string>
|
<string name="invite_to_app">Convida a Conversations</string>
|
||||||
<string name="unable_to_parse_invite">Non se puido enviar o convite</string>
|
<string name="unable_to_parse_invite">Non se puido enviar o convite</string>
|
||||||
<string name="server_does_not_support_easy_onboarding_invites">O servidor non soporta a creación de convites</string>
|
<string name="server_does_not_support_easy_onboarding_invites">O servidor non soporta a creación de convites</string>
|
||||||
|
<string name="no_active_accounts_support_this">Ningunha conta activa soporta esta función</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -948,4 +948,7 @@
|
||||||
<string name="failed_deliveries">Recapiti falliti</string>
|
<string name="failed_deliveries">Recapiti falliti</string>
|
||||||
<string name="more_options">Altre opzioni</string>
|
<string name="more_options">Altre opzioni</string>
|
||||||
<string name="no_application_found">Nessuna applicazione trovata</string>
|
<string name="no_application_found">Nessuna applicazione trovata</string>
|
||||||
|
<string name="invite_to_app">Invita su Conversations</string>
|
||||||
|
<string name="unable_to_parse_invite">Impossibile analizzare l\'invito</string>
|
||||||
|
<string name="server_does_not_support_easy_onboarding_invites">Il server non supporta la generazione di inviti</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -970,4 +970,8 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż
|
||||||
<string name="failed_deliveries">Nie dostarczone wiadomości</string>
|
<string name="failed_deliveries">Nie dostarczone wiadomości</string>
|
||||||
<string name="more_options">Więcej ustawień</string>
|
<string name="more_options">Więcej ustawień</string>
|
||||||
<string name="no_application_found">Nie znaleziono żadnej aplikacji</string>
|
<string name="no_application_found">Nie znaleziono żadnej aplikacji</string>
|
||||||
|
<string name="invite_to_app">Zaproś do Conversations</string>
|
||||||
|
<string name="unable_to_parse_invite">Nie można przetworzyć zaproszenia</string>
|
||||||
|
<string name="server_does_not_support_easy_onboarding_invites">Serwer nie wspiera tworzenia zaproszeń</string>
|
||||||
|
<string name="no_active_accounts_support_this">Nie ma aktywnych kont wspierających tę funkcję</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -947,4 +947,9 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="failed_deliveries">Entregas não efetuadas</string>
|
<string name="failed_deliveries">Entregas não efetuadas</string>
|
||||||
<string name="more_options">Mais opções</string>
|
<string name="more_options">Mais opções</string>
|
||||||
|
<string name="no_application_found">Não foi encontrado nenhum aplicativo</string>
|
||||||
|
<string name="invite_to_app">Convidar para o Conversations</string>
|
||||||
|
<string name="unable_to_parse_invite">Não foi possível processar o convite</string>
|
||||||
|
<string name="server_does_not_support_easy_onboarding_invites">O servidor não suporta a criação de convites</string>
|
||||||
|
<string name="no_active_accounts_support_this">Nenhuma conta ativa suporta esse recurso</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -961,4 +961,5 @@
|
||||||
<string name="invite_to_app">Invitați la Conversations</string>
|
<string name="invite_to_app">Invitați la Conversations</string>
|
||||||
<string name="unable_to_parse_invite">Nu s-a putut procesa invitația</string>
|
<string name="unable_to_parse_invite">Nu s-a putut procesa invitația</string>
|
||||||
<string name="server_does_not_support_easy_onboarding_invites">Serverul nu suportă generarea de invitații</string>
|
<string name="server_does_not_support_easy_onboarding_invites">Serverul nu suportă generarea de invitații</string>
|
||||||
|
<string name="no_active_accounts_support_this">Nici un cont activ nu suporta această caracteristică</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -974,4 +974,5 @@
|
||||||
<string name="invite_to_app">Conversations\'a davet et</string>
|
<string name="invite_to_app">Conversations\'a davet et</string>
|
||||||
<string name="unable_to_parse_invite">Davet iletilemedi</string>
|
<string name="unable_to_parse_invite">Davet iletilemedi</string>
|
||||||
<string name="server_does_not_support_easy_onboarding_invites">Sunucu, davet oluşturulmasını desteklemiyor</string>
|
<string name="server_does_not_support_easy_onboarding_invites">Sunucu, davet oluşturulmasını desteklemiyor</string>
|
||||||
|
<string name="no_active_accounts_support_this">Bu özelliği destekleyen aktif bir hesap yok</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -953,4 +953,5 @@
|
||||||
<string name="invite_to_app">Invite to Conversations</string>
|
<string name="invite_to_app">Invite to Conversations</string>
|
||||||
<string name="unable_to_parse_invite">Unable to parse invite</string>
|
<string name="unable_to_parse_invite">Unable to parse invite</string>
|
||||||
<string name="server_does_not_support_easy_onboarding_invites">Server does not support generating invites</string>
|
<string name="server_does_not_support_easy_onboarding_invites">Server does not support generating invites</string>
|
||||||
|
<string name="no_active_accounts_support_this">No active accounts support this feature</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue