aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java6
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java154
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Conversation.java92
-rw-r--r--src/main/java/eu/siacs/conversations/entities/MucOptions.java13
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Roster.java16
-rw-r--r--src/main/java/eu/siacs/conversations/generator/IqGenerator.java7
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java14
-rw-r--r--src/main/java/eu/siacs/conversations/parser/PresenceParser.java8
-rw-r--r--src/main/java/eu/siacs/conversations/services/ExportLogsService.java2
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java82
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java2
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java2
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationActivity.java25
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java9
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java9
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ExportLogsPreference.java7
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java4
-rw-r--r--src/main/java/eu/siacs/conversations/ui/SettingsActivity.java41
-rw-r--r--src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java3
-rw-r--r--src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java190
-rw-r--r--src/main/java/eu/siacs/conversations/ui/XmppActivity.java16
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java2
-rw-r--r--src/main/res/layout/activity_trust_keys.xml29
-rw-r--r--src/main/res/layout/keys_card.xml39
-rw-r--r--src/main/res/values/strings.xml4
-rw-r--r--src/main/res/xml/preferences.xml6
26 files changed, 541 insertions, 241 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 54fe7e45a..2bd0967c9 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -22,10 +22,6 @@ public final class Config {
return (ENCRYPTION_MASK & OPENPGP) != 0;
}
- public static boolean supportOpenPgpOnly() {
- return supportOpenPgp() && !multipleEncryptionChoices();
- }
-
public static boolean supportOtr() {
return (ENCRYPTION_MASK & OTR) != 0;
}
@@ -43,8 +39,8 @@ public final class Config {
public static final String DOMAIN_LOCK = "pix-art.de"; //only allow account creation for this domain
public static final String CONFERENCE_DOMAIN_LOCK = null; //only allow conference creation for this domain
public static final boolean LOCK_DOMAINS_IN_CONVERSATIONS = false; //only add contacts and conferences for own domains
+ public static final boolean LOCK_SETTINGS = true; //set to true to disallow account and settings editing
- public static final boolean SINGLE_ACCOUNT = true; //set to true to allow only one account
public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox
public static final boolean ALLOW_NON_TLS_CONNECTIONS = false; //very dangerous. you should have a good reason to set this to true
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
index 949975aa4..cc5c2491b 100644
--- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
@@ -81,13 +81,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
}
- public boolean fetchMapHasErrors(Contact contact) {
- Jid jid = contact.getJid().toBareJid();
- if (deviceIds.get(jid) != null) {
- for (Integer foreignId : this.deviceIds.get(jid)) {
- AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
- if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
- return true;
+ public boolean fetchMapHasErrors(List<Jid> jids) {
+ for(Jid jid : jids) {
+ if (deviceIds.get(jid) != null) {
+ for (Integer foreignId : this.deviceIds.get(jid)) {
+ AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
+ if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
+ return true;
+ }
}
}
}
@@ -242,12 +243,29 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
}
- public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) {
- return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust);
+ public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) {
+ return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toString(), trust);
+ }
+
+ public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) {
+ Set<IdentityKey> keys = new HashSet<>();
+ for(Jid jid : jids) {
+ keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), trust));
+ }
+ return keys;
}
- public long getNumTrustedKeys(Contact contact) {
- return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString());
+ public long getNumTrustedKeys(Jid jid) {
+ return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString());
+ }
+
+ public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
+ for(Jid jid : jids) {
+ if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString()) == 0) {
+ return true;
+ }
+ }
+ return false;
}
private AxolotlAddress getAddressForJid(Jid jid) {
@@ -259,11 +277,19 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return new HashSet<>(this.sessions.getAll(ownAddress).values());
}
- private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
+ private Set<XmppAxolotlSession> findSessionsForContact(Contact contact) {
AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
return new HashSet<>(this.sessions.getAll(contactAddress).values());
}
+ private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
+ HashSet<XmppAxolotlSession> sessions = new HashSet<>();
+ for(Jid jid : conversation.getAcceptedCryptoTargets()) {
+ sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values());
+ }
+ return sessions;
+ }
+
public Set<String> getFingerprintsForOwnSessions() {
Set<String> fingerprints = new HashSet<>();
for (XmppAxolotlSession session : findOwnSessions()) {
@@ -274,15 +300,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public Set<String> getFingerprintsForContact(final Contact contact) {
Set<String> fingerprints = new HashSet<>();
- for (XmppAxolotlSession session : findSessionsforContact(contact)) {
+ for (XmppAxolotlSession session : findSessionsForContact(contact)) {
fingerprints.add(session.getFingerprint());
}
return fingerprints;
}
- private boolean hasAny(Contact contact) {
- AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
- return sessions.hasAny(contactAddress);
+ private boolean hasAny(Jid jid) {
+ return sessions.hasAny(getAddressForJid(jid));
}
public boolean isPepBroken() {
@@ -594,10 +619,25 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
});
}
- public boolean isContactAxolotlCapable(Contact contact) {
- Jid jid = contact.getJid().toBareJid();
- return hasAny(contact) ||
- (deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
+ public boolean isConversationAxolotlCapable(Conversation conversation) {
+ final List<Jid> jids = getCryptoTargets(conversation);
+ for(Jid jid : jids) {
+ if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) {
+ return false;
+ }
+ }
+ return jids.size() > 0;
+ }
+
+ public List<Jid> getCryptoTargets(Conversation conversation) {
+ final List<Jid> jids;
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ jids = Arrays.asList(conversation.getJid().toBareJid());
+ } else {
+ jids = conversation.getMucOptions().getMembers();
+ jids.remove(account.getJid().toBareJid());
+ }
+ return jids;
}
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
@@ -753,33 +793,31 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
- return findDevicesWithoutSession(conversation.getContact().getJid().toBareJid());
- }
-
- public Set<AxolotlAddress> findDevicesWithoutSession(final Jid contactJid) {
- Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + contactJid);
Set<AxolotlAddress> addresses = new HashSet<>();
- if (deviceIds.get(contactJid) != null) {
- for (Integer foreignId : this.deviceIds.get(contactJid)) {
- AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId);
- if (sessions.get(address) == null) {
- IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
- if (identityKey != null) {
- Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
- XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
- sessions.put(address, session);
- } else {
- Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + contactJid + ":" + foreignId);
- if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
- addresses.add(address);
+ for(Jid jid : getCryptoTargets(conversation)) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid);
+ if (deviceIds.get(jid) != null) {
+ for (Integer foreignId : this.deviceIds.get(jid)) {
+ AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
+ if (sessions.get(address) == null) {
+ IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
+ if (identityKey != null) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
+ XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
+ sessions.put(address, session);
} else {
- Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken");
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId);
+ if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
+ addresses.add(address);
+ } else {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken");
+ }
}
}
}
+ } else {
+ Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
}
- } else {
- Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
}
if (deviceIds.get(account.getJid().toBareJid()) != null) {
for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
@@ -827,7 +865,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public boolean trustedSessionVerified(final Conversation conversation) {
- Set<XmppAxolotlSession> sessions = findSessionsforContact(conversation.getContact());
+ Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation);
sessions.addAll(findOwnSessions());
boolean verified = false;
for(XmppAxolotlSession session : sessions) {
@@ -842,26 +880,32 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return verified;
}
- public boolean hasPendingKeyFetches(Account account, Contact contact) {
+ public boolean hasPendingKeyFetches(Account account, List<Jid> jids) {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
- AxolotlAddress foreignAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
- return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
- || fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
-
+ if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) {
+ return true;
+ }
+ for(Jid jid : jids) {
+ AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toString(), 0);
+ if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
+ return true;
+ }
+ }
+ return false;
}
@Nullable
- private XmppAxolotlMessage buildHeader(Contact contact) {
+ private XmppAxolotlMessage buildHeader(Conversation conversation) {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
- contact.getJid().toBareJid(), getOwnDeviceId());
+ account.getJid().toBareJid(), getOwnDeviceId());
- Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
+ Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
Set<XmppAxolotlSession> ownSessions = findOwnSessions();
- if (contactSessions.isEmpty()) {
+ if (remoteSessions.isEmpty()) {
return null;
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
- for (XmppAxolotlSession session : contactSessions) {
+ for (XmppAxolotlSession session : remoteSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session);
}
@@ -876,7 +920,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
@Nullable
public XmppAxolotlMessage encrypt(Message message) {
- XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
+ XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation());
if (axolotlMessage != null) {
final String content;
@@ -913,11 +957,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
});
}
- public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
+ public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
executor.execute(new Runnable() {
@Override
public void run() {
- XmppAxolotlMessage axolotlMessage = buildHeader(contact);
+ XmppAxolotlMessage axolotlMessage = buildHeader(conversation);
onMessageCreatedCallback.run(axolotlMessage);
}
});
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index 7aa192696..dfc371864 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -9,11 +9,13 @@ import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
+import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.interfaces.DSAPublicKey;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
@@ -49,6 +51,7 @@ public class Conversation extends AbstractEntity implements Blockable {
public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
+ public static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
private String name;
private String contactUuid;
@@ -314,6 +317,18 @@ public class Conversation extends AbstractEntity implements Blockable {
return getLongAttribute("last_clear_history", 0);
}
+ public List<Jid> getAcceptedCryptoTargets() {
+ if (mode == MODE_SINGLE) {
+ return Arrays.asList(getJid().toBareJid());
+ } else {
+ return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
+ }
+ }
+
+ public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
+ setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
+ }
+
public void setCorrectingMessage(Message correctingMessage) {
this.correctingMessage = correctingMessage;
}
@@ -673,8 +688,8 @@ public class Conversation extends AbstractEntity implements Blockable {
final AxolotlService axolotlService = getAccount().getAxolotlService();
int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
if (next == -1) {
- if (Config.X509_VERIFICATION && mode == MODE_SINGLE) {
- if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
+ if (Config.X509_VERIFICATION) {
+ if (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) {
return Message.ENCRYPTION_AXOLOTL;
} else {
return Message.ENCRYPTION_NONE;
@@ -687,16 +702,20 @@ public class Conversation extends AbstractEntity implements Blockable {
next = outgoing;
}
}
- if (!Config.supportUnencrypted()
- && (mode == MODE_SINGLE || Config.supportOpenPgpOnly())
- && next <= 0) {
- if (Config.supportOmemo() && (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact()) || !Config.multipleEncryptionChoices())) {
+
+ if (!Config.supportUnencrypted() && next <= 0) {
+ if (Config.supportOmemo()
+ && (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
return Message.ENCRYPTION_AXOLOTL;
- } else if (Config.supportOtr()) {
+ } else if (Config.supportOtr() && mode == MODE_SINGLE) {
return Message.ENCRYPTION_OTR;
- } else if (Config.supportOpenPgp()) {
+ } else if (Config.supportOpenPgp()
+ && (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
return Message.ENCRYPTION_PGP;
}
+ } else if (next == Message.ENCRYPTION_AXOLOTL
+ && (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
+ next = Message.ENCRYPTION_NONE;
}
return next;
}
@@ -804,20 +823,59 @@ public class Conversation extends AbstractEntity implements Blockable {
}
public boolean setAttribute(String key, String value) {
- try {
- this.attributes.put(key, value);
- return true;
- } catch (JSONException e) {
- return false;
+ synchronized (this.attributes) {
+ try {
+ this.attributes.put(key, value);
+ return true;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+ }
+
+ public boolean setAttribute(String key, List<Jid> jids) {
+ JSONArray array = new JSONArray();
+ for(Jid jid : jids) {
+ array.put(jid.toBareJid().toString());
+ }
+ synchronized (this.attributes) {
+ try {
+ this.attributes.put(key, array);
+ return true;
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return false;
+ }
}
}
public String getAttribute(String key) {
- try {
- return this.attributes.getString(key);
- } catch (JSONException e) {
- return null;
+ synchronized (this.attributes) {
+ try {
+ return this.attributes.getString(key);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+ }
+
+ public List<Jid> getJidListAttribute(String key) {
+ ArrayList<Jid> list = new ArrayList<>();
+ synchronized (this.attributes) {
+ try {
+ JSONArray array = this.attributes.getJSONArray(key);
+ for (int i = 0; i < array.length(); ++i) {
+ try {
+ list.add(Jid.fromString(array.getString(i)));
+ } catch (InvalidJidException e) {
+ //ignored
+ }
+ }
+ } catch (JSONException e) {
+ //ignored
+ }
}
+ return list;
}
public int getIntAttribute(String key, int defaultValue) {
diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
index d23041416..380ed0667 100644
--- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java
+++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
@@ -4,9 +4,11 @@ import android.annotation.SuppressLint;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.xmpp.forms.Data;
@@ -245,7 +247,8 @@ public class MucOptions {
private Account account;
private final Map<String, User> users = Collections.synchronizedMap(new LinkedHashMap<String, User>());
- private List<String> features = new ArrayList<>();
+ private final Set<Jid> members = Collections.synchronizedSet(new HashSet<Jid>());
+ private final List<String> features = new ArrayList<>();
private Data form = new Data();
private Conversation conversation;
private boolean isOnline = false;
@@ -501,4 +504,12 @@ public class MucOptions {
public Conversation getConversation() {
return this.conversation;
}
+
+ public void putMember(Jid jid) {
+ members.add(jid);
+ }
+
+ public List<Jid> getMembers() {
+ return new ArrayList<>(members);
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java
index 628a31d1c..d3ad91816 100644
--- a/src/main/java/eu/siacs/conversations/entities/Roster.java
+++ b/src/main/java/eu/siacs/conversations/entities/Roster.java
@@ -9,7 +9,7 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class Roster {
final Account account;
- final HashMap<String, Contact> contacts = new HashMap<>();
+ final HashMap<Jid, Contact> contacts = new HashMap<>();
private String version = null;
public Roster(Account account) {
@@ -21,7 +21,7 @@ public class Roster {
return null;
}
synchronized (this.contacts) {
- Contact contact = contacts.get(jid.toBareJid().toString());
+ Contact contact = contacts.get(jid.toBareJid());
if (contact != null && contact.showInRoster()) {
return contact;
} else {
@@ -32,15 +32,13 @@ public class Roster {
public Contact getContact(final Jid jid) {
synchronized (this.contacts) {
- final Jid bareJid = jid.toBareJid();
- if (contacts.containsKey(bareJid.toString())) {
- return contacts.get(bareJid.toString());
- } else {
- Contact contact = new Contact(bareJid);
+ if (!contacts.containsKey(jid.toBareJid())) {
+ Contact contact = new Contact(jid.toBareJid());
contact.setAccount(account);
- contacts.put(bareJid.toString(), contact);
+ contacts.put(contact.getJid().toBareJid(), contact);
return contact;
}
+ return contacts.get(jid.toBareJid());
}
}
@@ -80,7 +78,7 @@ public class Roster {
contact.setAccount(account);
contact.setOption(Contact.Options.IN_ROSTER);
synchronized (this.contacts) {
- contacts.put(contact.getJid().toBareJid().toString(), contact);
+ contacts.put(contact.getJid().toBareJid(), contact);
}
}
diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
index c6b4a4502..15e27fc06 100644
--- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
@@ -336,4 +336,11 @@ public class IqGenerator extends AbstractGenerator {
enable.addChild(data);
return packet;
}
+
+ public IqPacket queryAffiliation(Conversation conversation, String affiliation) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+ packet.setTo(conversation.getJid().toBareJid());
+ packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation",affiliation);
+ return packet;
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index aa9d7f6c3..fd2d070ce 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -103,7 +103,7 @@ public class MessageParser extends AbstractParser implements
}
}
- private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) {
+ private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status) {
Message finishedMessage = null;
AxolotlService service = conversation.getAccount().getAxolotlService();
XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
@@ -357,7 +357,17 @@ public class MessageParser extends AbstractParser implements
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
- message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
+ Jid origin;
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ origin = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart());
+ if (origin == null) {
+ Log.d(Config.LOGTAG,"axolotl message in non anonymous conference received");
+ return;
+ }
+ } else {
+ origin = from;
+ }
+ message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status);
if (message == null) {
return;
}
diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
index 63d28c97b..c2782d232 100644
--- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
@@ -64,10 +64,14 @@ public class PresenceParser extends AbstractParser implements
Element item = x.findChild("item");
if (item != null && !from.isBareJid()) {
mucOptions.setError(MucOptions.Error.NONE);
- MucOptions.User user = new MucOptions.User(mucOptions,from);
+ MucOptions.User user = new MucOptions.User(mucOptions, from);
user.setAffiliation(item.getAttribute("affiliation"));
user.setRole(item.getAttribute("role"));
- user.setJid(item.getAttributeAsJid("jid"));
+ Jid real = item.getAttributeAsJid("jid");
+ if (real != null) {
+ user.setJid(real);
+ mucOptions.putMember(real.toBareJid());
+ }
if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(mucOptions.getConversation().getJid())) {
mucOptions.setOnline();
mucOptions.setSelf(user);
diff --git a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
index 76983a905..87e65931b 100644
--- a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
+++ b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
@@ -45,9 +45,9 @@ public class ExportLogsService extends Service {
new Thread(new Runnable() {
@Override
public void run() {
- running.set(false);
export();
stopForeground(true);
+ running.set(false);
stopSelf();
}
}).start();
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 8df3808c7..3f0b467ab 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -254,6 +254,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
@Override
public void onBind(final Account account) {
+ synchronized (mInProgressAvatarFetches) {
+ for (Iterator<String> iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
+ final String KEY = iterator.next();
+ if (KEY.startsWith(account.getJid().toBareJid() + "_")) {
+ iterator.remove();
+ }
+ }
+ }
account.getRoster().clearPresences();
mJingleConnectionManager.cancelInTransmission();
fetchRosterFromServer(account);
@@ -611,6 +619,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
return getPreferences().getBoolean("xa_on_silent_mode", false);
}
+ private boolean treatVibrateAsSilent() {
+ return getPreferences().getBoolean("treat_vibrate_as_silent", false);
+ }
+
private boolean awayWhenScreenOff() {
return getPreferences().getBoolean("away_when_screen_off", false);
}
@@ -645,7 +657,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private boolean isPhoneSilenced() {
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- return audioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT;
+ if (treatVibrateAsSilent()) {
+ return audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
+ } else {
+ return audioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT;
+ }
}
private void resetAllAttemptCounts(boolean reallyAll) {
@@ -1782,8 +1798,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private void join(Conversation conversation) {
Account account = conversation.getAccount();
- final String nick = conversation.getMucOptions().getProposedNick();
- final Jid joinJid = conversation.getMucOptions().createJoinJid(nick);
+ final MucOptions mucOptions = conversation.getMucOptions();
+ final Jid joinJid = mucOptions.getSelf().getFullJid();
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
PresencePacket packet = new PresencePacket();
packet.setFrom(conversation.getAccount().getJid());
@@ -1793,7 +1809,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
x.addChild("password").setContent(conversation.getMucOptions().getPassword());
}
- if (conversation.getMucOptions().mamSupport()) {
+ if (mucOptions.mamSupport()) {
// Use MAM instead of the limited muc history to get history
x.addChild("history").setAttribute("maxchars", "0");
} else {
@@ -1812,9 +1828,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
conversation.setContactJid(joinJid);
databaseBackend.updateConversation(conversation);
}
- if (conversation.getMucOptions().mamSupport()) {
+
+ if (mucOptions.mamSupport()) {
getMessageArchiveService().catchupMUC(conversation);
}
+ if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
+ fetchConferenceMembers(conversation);
+ }
sendUnsentMessages(conversation);
}
@@ -1838,6 +1858,37 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
+ private void fetchConferenceMembers(final Conversation conversation) {
+ final Account account = conversation.getAccount();
+ final String[] affiliations = {"member","admin","owner"};
+ OnIqPacketReceived callback = new OnIqPacketReceived() {
+
+ private int i = 0;
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ Element query = packet.query("http://jabber.org/protocol/muc#admin");
+ if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
+ for(Element child : query.getChildren()) {
+ if ("item".equals(child.getName())) {
+ conversation.getMucOptions().putMember(child.getAttributeAsJid("jid"));
+ }
+ }
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid());
+ }
+ ++i;
+ if (i >= affiliations.length) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
+ }
+ }
+ };
+ for(String affiliation : affiliations) {
+ sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
+ }
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching members for "+conversation.getName());
+ }
+
public void providePasswordForMuc(Conversation conversation, String password) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
conversation.getMucOptions().setPassword(password);
@@ -1911,7 +1962,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
account.pendingConferenceLeaves.remove(conversation);
if (account.getStatus() == Account.State.ONLINE || now) {
PresencePacket packet = new PresencePacket();
- packet.setTo(conversation.getJid());
+ packet.setTo(conversation.getMucOptions().getSelf().getFullJid());
packet.setFrom(conversation.getAccount().getJid());
packet.setAttribute("type", "unavailable");
sendPresencePacket(conversation.getAccount(), packet);
@@ -2011,9 +2062,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Element query = packet.findChild("query","http://jabber.org/protocol/disco#info");
+ if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
ArrayList<String> features = new ArrayList<>();
- Element query = packet.query();
for (Element child : query.getChildren()) {
if (child != null && child.getName().equals("feature")) {
String var = child.getAttribute("var");
@@ -2030,6 +2081,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (callback != null) {
callback.onConferenceConfigurationFetched(conversation);
}
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetched muc configuration for "+conversation.getJid().toBareJid()+" - "+features.toString());
updateConversationUi();
} else if (packet.getType() == IqPacket.TYPE.ERROR) {
if (callback != null) {
@@ -2510,22 +2562,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
synchronized (account) {
XmppConnection connection = account.getXmppConnection();
- if (connection != null) {
- disconnect(account, force);
- } else {
+ if (connection == null) {
connection = createConnection(account);
account.setXmppConnection(connection);
}
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
- synchronized (this.mInProgressAvatarFetches) {
- for (Iterator<String> iterator = this.mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
- final String KEY = iterator.next();
- if (KEY.startsWith(account.getJid().toBareJid() + "_")) {
- iterator.remove();
- }
- }
- }
if (!force) {
+ disconnect(account, false);
try {
Log.d(Config.LOGTAG, "wait for disconnect");
Thread.sleep(500); //sleep wait for disconnect
@@ -2538,6 +2581,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
thread.start();
scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
} else {
+ disconnect(account, force);
account.getRoster().clearPresences();
connection.resetEverything();
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
index a83ca8bab..adbb09537 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
@@ -333,7 +333,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.muc_details, menu);
- return true;
+ return super.onCreateOptionsMenu(menu);
}
@Override
diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
index c0e248376..b2d9e1d83 100644
--- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -297,7 +297,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
edit.setVisible(false);
delete.setVisible(false);
}
- return true;
+ return super.onCreateOptionsMenu(menu);
}
private void populateView() {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
index 5594d6d19..14f6ac3f4 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
@@ -496,7 +496,7 @@ public class ConversationActivity extends XmppActivity
//menuContactDetails.setVisible(false);
menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating());
menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
- menuSecure.setVisible(Config.supportOpenPgp() && Config.multipleEncryptionChoices()); //only if pgp is supported we have a choice
+ menuSecure.setVisible((Config.supportOpenPgp() || Config.supportOmemo()) && Config.multipleEncryptionChoices()); //only if pgp is supported we have a choice
} else {
//menuMucDetails.setVisible(false);
menuSecure.setVisible(Config.multipleEncryptionChoices());
@@ -508,7 +508,7 @@ public class ConversationActivity extends XmppActivity
}
}
}
- return true;
+ return super.onCreateOptionsMenu(menu);
}
protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
@@ -946,8 +946,8 @@ public class ConversationActivity extends XmppActivity
axolotl.setVisible(Config.supportOmemo());
if (conversation.getMode() == Conversation.MODE_MULTI) {
otr.setVisible(false);
- axolotl.setVisible(false);
- } else if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
+ }
+ if (!conversation.getAccount().getAxolotlService().isConversationAxolotlCapable(conversation)) {
axolotl.setEnabled(false);
}
switch (conversation.getNextEncryption()) {
@@ -1621,18 +1621,23 @@ public class ConversationActivity extends XmppActivity
protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
- Contact contact = mSelectedConversation.getContact();
+ final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
+ boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED).isEmpty();
- boolean hasUndecidedContact = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED,contact).isEmpty();
+ boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, targets).isEmpty();
boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
- boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0;
- if(hasUndecidedOwn || hasUndecidedContact || hasPendingKeys || hasNoTrustedKeys) {
+ boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
+ if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
axolotlService.createSessionsIfNeeded(mSelectedConversation);
Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
- intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString());
+ String[] contacts = new String[targets.size()];
+ for(int i = 0; i < contacts.length; ++i) {
+ contacts[i] = targets.get(i).toString();
+ }
+ intent.putExtra("contacts", contacts);
intent.putExtra(EXTRA_ACCOUNT, mSelectedConversation.getAccount().getJid().toBareJid().toString());
intent.putExtra("choice", attachmentChoice);
- intent.putExtra("has_no_trusted", hasNoTrustedKeys);
+ intent.putExtra("conversation",mSelectedConversation.getUuid());
startActivityForResult(intent, requestCode);
return true;
} else {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index cb7ad86bb..15e75d7f5 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -123,7 +123,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private int getIndexOf(String uuid, List<Message> messages) {
if (uuid == null) {
- return 0;
+ return messages.size() - 1;
}
for(int i = 0; i < messages.size(); ++i) {
if (uuid.equals(messages.get(i).getUuid())) {
@@ -164,7 +164,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
@Override
public void run() {
final int oldPosition = messagesView.getFirstVisiblePosition();
- Message message = messageList.get(oldPosition);
+ final Message message;
+ if (oldPosition < messageList.size()) {
+ message = messageList.get(oldPosition);
+ } else {
+ message = null;
+ }
String uuid = message != null ? message.getUuid() : null;
View v = messagesView.getChildAt(0);
final int pxOffset = (v == null) ? 0 : v.getTop();
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
index 7b6167178..b05e4e44a 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -499,7 +499,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
clearDevices.setVisible(false);
mamPrefs.setVisible(false);
}
- return true;
+ return super.onCreateOptionsMenu(menu);
}
@Override
@@ -621,6 +621,13 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mNamePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE);
}
+ mPassword.setEnabled(!Config.LOCK_SETTINGS);
+ mAccountJid.setEnabled(!Config.LOCK_SETTINGS);
+ mHostname.setEnabled(!Config.LOCK_SETTINGS);
+ mPort.setEnabled(!Config.LOCK_SETTINGS);
+ mPasswordConfirm.setEnabled(!Config.LOCK_SETTINGS);
+ mRegisterNew.setEnabled(!Config.LOCK_SETTINGS);
+
if (!mInitMode) {
this.mAvatar.setVisibility(View.VISIBLE);
this.mAvatar.setImageBitmap(avatarService().get(this.mAccount, getPixel(Config.AVATAR_SIZE)));
diff --git a/src/main/java/eu/siacs/conversations/ui/ExportLogsPreference.java b/src/main/java/eu/siacs/conversations/ui/ExportLogsPreference.java
index a4e178bd3..bedb4172d 100644
--- a/src/main/java/eu/siacs/conversations/ui/ExportLogsPreference.java
+++ b/src/main/java/eu/siacs/conversations/ui/ExportLogsPreference.java
@@ -1,7 +1,10 @@
package eu.siacs.conversations.ui;
+import android.Manifest;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
import android.preference.Preference;
import android.util.AttributeSet;
@@ -22,6 +25,10 @@ public class ExportLogsPreference extends Preference {
}
protected void onClick() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && getContext().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
final Intent startIntent = new Intent(getContext(), ExportLogsService.class);
getContext().startService(startIntent);
super.onClick();
diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
index 1fc1924dd..feac2c622 100644
--- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -155,9 +155,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
addAccount.setVisible(false);
addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
} else {
- addAccount.setVisible(!Config.SINGLE_ACCOUNT);
+ addAccount.setVisible(!Config.LOCK_SETTINGS);
}
- addAccountWithCertificate.setVisible(!Config.SINGLE_ACCOUNT);
+ addAccountWithCertificate.setVisible(!Config.LOCK_SETTINGS);
if (!accountsLeftToEnable()) {
enableAll.setVisible(false);
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index 07c328b9a..750a74212 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -3,8 +3,10 @@ package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.app.FragmentManager;
import android.content.DialogInterface;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.preference.ListPreference;
@@ -19,16 +21,20 @@ import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import java.util.Locale;
import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.ExportLogsService;
import eu.siacs.conversations.xmpp.XmppConnection;
public class SettingsActivity extends XmppActivity implements
OnSharedPreferenceChangeListener {
+
+ public static final int REQUEST_WRITE_LOGS = 0xbf8701;
private SettingsFragment mSettingsFragment;
@Override
@@ -130,6 +136,15 @@ public class SettingsActivity extends XmppActivity implements
return true;
}
});
+
+ final Preference exportLogsPreference = mSettingsFragment.findPreference("export_logs");
+ exportLogsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ hasStoragePermission(REQUEST_WRITE_LOGS);
+ return true;
+ }
+ });
}
@Override
@@ -140,8 +155,13 @@ public class SettingsActivity extends XmppActivity implements
}
@Override
- public void onSharedPreferenceChanged(SharedPreferences preferences,
- String name) {
+ public void onSharedPreferenceChanged(SharedPreferences preferences, String name) {
+ final List<String> resendPresence = Arrays.asList(
+ "confirm_messages",
+ "xa_on_silent_mode",
+ "away_when_screen_off",
+ "allow_message_correction",
+ "treat_vibrate_as_silent");
if (name.equals("resource")) {
String resource = preferences.getString("resource", "mobile")
.toLowerCase(Locale.US);
@@ -160,10 +180,7 @@ public class SettingsActivity extends XmppActivity implements
}
} else if (name.equals("keep_foreground_service")) {
xmppConnectionService.toggleForegroundService();
- } else if (name.equals("confirm_messages")
- || name.equals("xa_on_silent_mode")
- || name.equals("away_when_screen_off")
- || name.equals("allow_message_correction")) {
+ } else if (resendPresence.contains(name)) {
if (xmppConnectionServiceBound) {
if (name.equals("away_when_screen_off")) {
xmppConnectionService.toggleScreenEventReceiver();
@@ -179,6 +196,18 @@ public class SettingsActivity extends XmppActivity implements
}
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ if (grantResults.length > 0)
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (requestCode == REQUEST_WRITE_LOGS) {
+ getApplicationContext().startService(new Intent(getApplicationContext(), ExportLogsService.class));
+ }
+ } else {
+ Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
+ }
+ }
+
private void displayToast(final String msg) {
runOnUiThread(new Runnable() {
@Override
diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
index 48baf914e..7d650e5bb 100644
--- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
@@ -533,7 +533,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
mSearchEditText.append(mInitialJid);
filter(mInitialJid);
}
- return true;
+ return super.onCreateOptionsMenu(menu);
}
@Override
@@ -740,6 +740,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
Presence.Status s = p == null ? Presence.Status.OFFLINE : p.getStatus();
if (contact.showInRoster() && contact.match(needle)
&& (!this.mHideOfflineContacts
+ || (needle != null && !needle.trim().isEmpty())
|| s.compareTo(Presence.Status.OFFLINE) < 0)) {
this.contacts.add(contact);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
index eec30798e..cc4ba7b23 100644
--- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
@@ -12,7 +12,9 @@ import android.widget.Toast;
import org.whispersystems.libaxolotl.IdentityKey;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -20,32 +22,29 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
- private Jid accountJid;
- private Jid contactJid;
+ private List<Jid> contactJids;
- private Contact contact;
private Account mAccount;
+ private Conversation mConversation;
private TextView keyErrorMessage;
private LinearLayout keyErrorMessageCard;
private TextView ownKeysTitle;
private LinearLayout ownKeys;
private LinearLayout ownKeysCard;
- private TextView foreignKeysTitle;
private LinearLayout foreignKeys;
- private LinearLayout foreignKeysCard;
private Button mSaveButton;
private Button mCancelButton;
private AxolotlService.FetchStatus lastFetchReport = AxolotlService.FetchStatus.SUCCESS;
private final Map<String, Boolean> ownKeysToTrust = new HashMap<>();
- private final Map<String, Boolean> foreignKeysToTrust = new HashMap<>();
+ private final Map<Jid,Map<String, Boolean>> foreignKeysToTrust = new HashMap<>();
private final OnClickListener mSaveButtonListener = new OnClickListener() {
@Override
@@ -70,25 +69,16 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
}
@Override
- protected String getShareableUri() {
- if (contact != null) {
- return contact.getShareableUri();
- } else {
- return "";
- }
- }
-
- @Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_trust_keys);
- try {
- this.accountJid = Jid.fromString(getIntent().getExtras().getString(EXTRA_ACCOUNT));
- } catch (final InvalidJidException ignored) {
- }
- try {
- this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact"));
- } catch (final InvalidJidException ignored) {
+ this.contactJids = new ArrayList<>();
+ for(String jid : getIntent().getStringArrayExtra("contacts")) {
+ try {
+ this.contactJids.add(Jid.fromString(jid));
+ } catch (InvalidJidException e) {
+ e.printStackTrace();
+ }
}
keyErrorMessageCard = (LinearLayout) findViewById(R.id.key_error_message_card);
@@ -96,9 +86,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
ownKeysTitle = (TextView) findViewById(R.id.own_keys_title);
ownKeys = (LinearLayout) findViewById(R.id.own_keys_details);
ownKeysCard = (LinearLayout) findViewById(R.id.own_keys_card);
- foreignKeysTitle = (TextView) findViewById(R.id.foreign_keys_title);
- foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys_details);
- foreignKeysCard = (LinearLayout) findViewById(R.id.foreign_keys_card);
+ foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys);
mCancelButton = (Button) findViewById(R.id.cancel_button);
mCancelButton.setOnClickListener(mCancelButtonListener);
mSaveButton = (Button) findViewById(R.id.save_button);
@@ -119,7 +107,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
boolean hasForeignKeys = false;
for(final String fingerprint : ownKeysToTrust.keySet()) {
hasOwnKeys = true;
- addFingerprintRowWithListeners(ownKeys, contact.getAccount(), fingerprint, false,
+ addFingerprintRowWithListeners(ownKeys, mAccount, fingerprint, false,
XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)), false,
new CompoundButton.OnCheckedChangeListener() {
@Override
@@ -132,30 +120,44 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
null
);
}
- for(final String fingerprint : foreignKeysToTrust.keySet()) {
- hasForeignKeys = true;
- addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), fingerprint, false,
- XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(fingerprint)), false,
- new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- foreignKeysToTrust.put(fingerprint, isChecked);
- lockOrUnlockAsNeeded();
- }
- },
- null,
- null
- );
- }
- if(hasOwnKeys) {
- ownKeysTitle.setText(accountJid.toString());
- ownKeysCard.setVisibility(View.VISIBLE);
- }
- if(hasForeignKeys) {
- foreignKeysTitle.setText(contactJid.toString());
- foreignKeysCard.setVisibility(View.VISIBLE);
+ synchronized (this.foreignKeysToTrust) {
+ for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
+ hasForeignKeys = true;
+ final LinearLayout layout = (LinearLayout) getLayoutInflater().inflate(R.layout.keys_card, foreignKeys, false);
+ final Jid jid = entry.getKey();
+ final TextView header = (TextView) layout.findViewById(R.id.foreign_keys_title);
+ final LinearLayout keysContainer = (LinearLayout) layout.findViewById(R.id.foreign_keys_details);
+ final TextView informNoKeys = (TextView) layout.findViewById(R.id.no_keys_to_accept);
+ header.setText(jid.toString());
+ final Map<String, Boolean> fingerprints = entry.getValue();
+ for (final String fingerprint : fingerprints.keySet()) {
+ addFingerprintRowWithListeners(keysContainer, mAccount, fingerprint, false,
+ XmppAxolotlSession.Trust.fromBoolean(fingerprints.get(fingerprint)), false,
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ fingerprints.put(fingerprint, isChecked);
+ lockOrUnlockAsNeeded();
+ }
+ },
+ null,
+ null
+ );
+ }
+ if (fingerprints.size() == 0) {
+ informNoKeys.setVisibility(View.VISIBLE);
+ informNoKeys.setText(getString(R.string.no_keys_just_confirm,mAccount.getRoster().getContact(jid).getDisplayName()));
+ } else {
+ informNoKeys.setVisibility(View.GONE);
+ }
+ foreignKeys.addView(layout);
+ }
}
+
+ ownKeysTitle.setText(mAccount.getJid().toBareJid().toString());
+ ownKeysCard.setVisibility(hasOwnKeys ? View.VISIBLE : View.GONE);
+ foreignKeys.setVisibility(hasForeignKeys ? View.VISIBLE : View.GONE);
if(hasPendingKeyFetches()) {
setFetching();
lock();
@@ -163,13 +165,15 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
if (!hasForeignKeys && hasNoOtherTrustedKeys()) {
keyErrorMessageCard.setVisibility(View.VISIBLE);
if (lastFetchReport == AxolotlService.FetchStatus.ERROR
- || contact.getAccount().getAxolotlService().fetchMapHasErrors(contact)) {
+ || mAccount.getAxolotlService().fetchMapHasErrors(contactJids)) {
keyErrorMessage.setText(R.string.error_no_keys_to_trust_server_error);
} else {
keyErrorMessage.setText(R.string.error_no_keys_to_trust);
}
- ownKeys.removeAllViews(); ownKeysCard.setVisibility(View.GONE);
- foreignKeys.removeAllViews(); foreignKeysCard.setVisibility(View.GONE);
+ ownKeys.removeAllViews();
+ ownKeysCard.setVisibility(View.GONE);
+ foreignKeys.removeAllViews();
+ foreignKeys.setVisibility(View.GONE);
}
lockOrUnlockAsNeeded();
setDone();
@@ -177,46 +181,58 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
}
private boolean reloadFingerprints() {
+ List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
ownKeysToTrust.clear();
- foreignKeysToTrust.clear();
AxolotlService service = this.mAccount.getAxolotlService();
Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
- Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, contact);
- if (hasNoOtherTrustedKeys() && ownKeysSet.size() == 0) {
- foreignKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, contact));
- }
for(final IdentityKey identityKey : ownKeysSet) {
if(!ownKeysToTrust.containsKey(identityKey)) {
ownKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
}
}
- for(final IdentityKey identityKey : foreignKeysSet) {
- if(!foreignKeysToTrust.containsKey(identityKey)) {
- foreignKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
+ synchronized (this.foreignKeysToTrust) {
+ foreignKeysToTrust.clear();
+ for (Jid jid : contactJids) {
+ Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, jid);
+ if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) {
+ foreignKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, jid));
+ }
+ Map<String, Boolean> foreignFingerprints = new HashMap<>();
+ for (final IdentityKey identityKey : foreignKeysSet) {
+ if (!foreignFingerprints.containsKey(identityKey)) {
+ foreignFingerprints.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
+ }
+ }
+ if (foreignFingerprints.size() > 0 || !acceptedTargets.contains(jid)) {
+ foreignKeysToTrust.put(jid, foreignFingerprints);
+ }
}
}
- return ownKeysSet.size() + foreignKeysSet.size() > 0;
+ return ownKeysSet.size() + foreignKeysToTrust.size() > 0;
}
@Override
public void onBackendConnected() {
- if ((accountJid != null) && (contactJid != null)) {
- this.mAccount = xmppConnectionService.findAccountByJid(accountJid);
- if (this.mAccount == null) {
- return;
- }
- this.contact = this.mAccount.getRoster().getContact(contactJid);
+ Intent intent = getIntent();
+ this.mAccount = extractAccount(intent);
+ if (this.mAccount != null && intent != null) {
+ String uuid = intent.getStringExtra("conversation");
+ this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
reloadFingerprints();
populateView();
}
}
private boolean hasNoOtherTrustedKeys() {
+ return mAccount == null || mAccount.getAxolotlService().anyTargetHasNoTrustedKeys(contactJids);
+ }
+
+ private boolean hasNoOtherTrustedKeys(Jid contact) {
return mAccount == null || mAccount.getAxolotlService().getNumTrustedKeys(contact) == 0;
}
private boolean hasPendingKeyFetches() {
- return mAccount != null && contact != null && mAccount.getAxolotlService().hasPendingKeyFetches(mAccount,contact);
+ return mAccount != null && mAccount.getAxolotlService().hasPendingKeyFetches(mAccount, contactJids);
}
@@ -262,14 +278,28 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
private void commitTrusts() {
for(final String fingerprint :ownKeysToTrust.keySet()) {
- contact.getAccount().getAxolotlService().setFingerprintTrust(
+ mAccount.getAxolotlService().setFingerprintTrust(
fingerprint,
XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)));
}
- for(final String fingerprint:foreignKeysToTrust.keySet()) {
- contact.getAccount().getAxolotlService().setFingerprintTrust(
- fingerprint,
- XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(fingerprint)));
+ List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
+ synchronized (this.foreignKeysToTrust) {
+ for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
+ Jid jid = entry.getKey();
+ Map<String, Boolean> value = entry.getValue();
+ if (!acceptedTargets.contains(jid)) {
+ acceptedTargets.add(jid);
+ }
+ for (final String fingerprint : value.keySet()) {
+ mAccount.getAxolotlService().setFingerprintTrust(
+ fingerprint,
+ XmppAxolotlSession.Trust.fromBoolean(value.get(fingerprint)));
+ }
+ }
+ }
+ if (mConversation != null && mConversation.getMode() == Conversation.MODE_MULTI) {
+ mConversation.setAcceptedCryptoTargets(acceptedTargets);
+ xmppConnectionService.updateConversation(mConversation);
}
}
@@ -284,11 +314,17 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
}
private void lockOrUnlockAsNeeded() {
- if (hasNoOtherTrustedKeys() && !foreignKeysToTrust.values().contains(true)){
- lock();
- } else {
- unlock();
+ synchronized (this.foreignKeysToTrust) {
+ for (Jid jid : contactJids) {
+ Map<String, Boolean> fingerprints = foreignKeysToTrust.get(jid);
+ if (hasNoOtherTrustedKeys(jid) && (fingerprints == null || !fingerprints.values().contains(true))) {
+ lock();
+ return;
+ }
+ }
}
+ unlock();
+
}
private void setDone() {
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
index d0024f9e6..3c109967b 100644
--- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -42,6 +42,7 @@ import android.preference.PreferenceManager;
import android.text.InputType;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -380,6 +381,19 @@ public abstract class XmppActivity extends Activity {
}
}
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ final MenuItem menuSettings = menu.findItem(R.id.action_settings);
+ final MenuItem menuManageAccounts = menu.findItem(R.id.action_accounts);
+ if (menuSettings != null) {
+ menuSettings.setVisible(!Config.LOCK_SETTINGS);
+ }
+ if (menuManageAccounts != null) {
+ menuManageAccounts.setVisible(!Config.LOCK_SETTINGS);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
protected boolean showBatteryOptimizationWarning() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
@@ -788,7 +802,7 @@ public abstract class XmppActivity extends Activity {
+ "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
+ "\n\n" + getString(R.string.purge_key_desc_part2));
builder.setNegativeButton(getString(R.string.cancel), null);
- builder.setPositiveButton(getString(R.string.accept),
+ builder.setPositiveButton(getString(R.string.purge_key),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
index 56582f974..a8e081a72 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -218,7 +218,7 @@ public class JingleConnection implements Transferable {
public void init(final Message message) {
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
Conversation conversation = message.getConversation();
- conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation.getContact(), new OnMessageCreatedCallback() {
+ conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, new OnMessageCreatedCallback() {
@Override
public void run(XmppAxolotlMessage xmppAxolotlMessage) {
if (xmppAxolotlMessage != null) {
diff --git a/src/main/res/layout/activity_trust_keys.xml b/src/main/res/layout/activity_trust_keys.xml
index 6f8a1bc9c..d7bb76281 100644
--- a/src/main/res/layout/activity_trust_keys.xml
+++ b/src/main/res/layout/activity_trust_keys.xml
@@ -80,34 +80,11 @@
</LinearLayout>
<LinearLayout
- android:id="@+id/foreign_keys_card"
+ android:id="@+id/foreign_keys"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:layout_marginLeft="@dimen/activity_horizontal_margin"
- android:layout_marginRight="@dimen/activity_horizontal_margin"
- android:layout_marginTop="@dimen/activity_vertical_margin"
- android:layout_marginBottom="@dimen/activity_vertical_margin"
- android:background="@drawable/infocard_border"
- android:orientation="vertical"
- android:padding="@dimen/infocard_padding"
- android:visibility="gone">
-
- <TextView
- android:id="@+id/foreign_keys_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textColor="@color/black87"
- android:textSize="?attr/TextSizeHeadline"
- android:textStyle="bold"/>
-
- <LinearLayout
- android:id="@+id/foreign_keys_details"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:divider="?android:dividerHorizontal"
- android:showDividers="middle"
- android:orientation="vertical">
- </LinearLayout>
+ android:visibility="gone"
+ android:orientation="vertical">
</LinearLayout>
diff --git a/src/main/res/layout/keys_card.xml b/src/main/res/layout/keys_card.xml
new file mode 100644
index 000000000..d3271d1bc
--- /dev/null
+++ b/src/main/res/layout/keys_card.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout android:id="@+id/foreign_keys_card"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/activity_vertical_margin"
+ android:layout_marginLeft="@dimen/activity_horizontal_margin"
+ android:layout_marginRight="@dimen/activity_horizontal_margin"
+ android:layout_marginTop="@dimen/activity_vertical_margin"
+ android:background="@drawable/infocard_border"
+ android:orientation="vertical"
+ android:padding="@dimen/infocard_padding">
+
+ <TextView
+ android:id="@+id/foreign_keys_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/black87"
+ android:textSize="?attr/TextSizeHeadline"
+ android:textStyle="bold"/>
+
+ <LinearLayout
+ android:id="@+id/foreign_keys_details"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:divider="?android:dividerHorizontal"
+ android:orientation="vertical"
+ android:showDividers="middle">
+ </LinearLayout>
+
+ <TextView
+ android:layout_marginTop="8dp"
+ android:id="@+id/no_keys_to_accept"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@color/black87"
+ android:text="@string/no_keys_just_confirm"
+ android:textSize="?attr/TextSizeBody"/>
+</LinearLayout> \ No newline at end of file
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 49f3730d6..9247ba075 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -550,6 +550,8 @@
<string name="pref_xa_on_silent_mode">Not available in silent mode</string>
<string name="update_info">Pix-Art Messenger is checking for an update. If an update is available you will be asked, if you want to update your version. The update process is downloading and installing the new version automatically.</string>
<string name="pref_xa_on_silent_mode_summary">Marks your resource as not available when device is in silent mode</string>
+ <string name="pref_treat_vibrate_as_silent">Treat vibrate as silent mode</string>
+ <string name="pref_treat_vibrate_as_silent_summary">Marks your resource as not available when device is on vibrate</string>
<string name="pref_show_connection_options">Extended connection settings</string>
<string name="pref_show_connection_options_summary">Show hostname and port settings when setting up an account</string>
<string name="hostname_example">xmpp.example.com</string>
@@ -622,5 +624,5 @@
<string name="no_participants">No participants</string>
<string name="correct_message">Correct message</string>
<string name="send_corrected_message">Send corrected message</string>
->>>>>>> refs/heads/pr/73
+ <string name="no_keys_just_confirm">You already trust this contact. By selecting \'done\' you are just confirming that %s is part of this conference.</string>
</resources>
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
index ca8847cba..4ffd5c5cf 100644
--- a/src/main/res/xml/preferences.xml
+++ b/src/main/res/xml/preferences.xml
@@ -209,6 +209,12 @@
android:key="xa_on_silent_mode"
android:summary="@string/pref_xa_on_silent_mode_summary"
android:title="@string/pref_xa_on_silent_mode"/>
+ <CheckBoxPreference
+ android:dependency="xa_on_silent_mode"
+ android:defaultValue="false"
+ android:key="treat_vibrate_as_silent"
+ android:title="@string/pref_treat_vibrate_as_silent"
+ android:summary="@string/pref_treat_vibrate_as_silent_summary"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_expert_options_other">
<CheckBoxPreference