diff options
49 files changed, 826 insertions, 378 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index f847e94d3..73cb0297f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ###Changelog +####Version 1.11.0 +* OMEMO encrypted conferences + +####Version 1.10.1 +* made message correction opt-in +* various bug fixes + ####Version 1.10.0 * Support for XEP-0357: Push Notifications * Support for XEP-0308: Last Message Correction @@ -261,7 +261,9 @@ I am available for hire. Contact me via XMPP: `inputmice@siacs.eu` #### Why are there three end-to-end encryption methods and which one should I choose? -In most cases OTR should be the encryption method of choice. It works out of the box with most contacts as long as they are online. However, openPGP can, in some cases, (message carbons to multiple clients) be more flexible. Unlike OTR, OMEMO works even when a contact is offline, and works with multiple devices. It also allows asynchronous file-transfer when the server has [HTTP File Upload](http://xmpp.org/extensions/xep-0363.html). However, OMEMO is not as widely supported as OTR and is currently implemented only by Conversations. OMEMO should be preferred over OTR for contacts who use Conversations. +* OTR is a legacy encryption method. It works out of the box with most contacts as long as they are online. +* OMEMO works even when a contact is offline, and works with multiple devices. It also allows asynchronous file-transfer when the server has [HTTP File Upload](http://xmpp.org/extensions/xep-0363.html). However, OMEMO is not as widely supported as OTR and is currently implemented only by Conversations and Gajim. OMEMO should be preferred over OTR for contacts who use Conversations. +* OpenPGP (XEP-0027) is a very old encryption method that has some advantages over OTR but should only be used by experts who know what they are doing. #### How do I use OpenPGP @@ -282,12 +284,28 @@ manage accounts and choose renew PGP announcement from the contextual menu. #### How does the encryption for conferences work? -For conferences the only supported encryption method is OpenPGP (OTR does not -work with multiple participants). Every participant has to announce their -OpenPGP key (see answer above). If you would like to send encrypted messages to -a conference you have to make sure that you have every participant's public key -in your OpenKeychain. Right now there is no check in Conversations to ensure -that. You have to take care of that yourself. Go to the conference details and +For conferences only OMEMO and OpenPGP are suppored as encryption method. (OTR +does not work with multiple participants). + +##### OMEMO + +OMEMO encryption works only in private (members only) conferences that are non-anonymous. +You need to have presence subscription with every member of the conference. +You can verify that by going into the conference details, long press every member and start +a conversation with them. (Or select 'contact details' if they are already in your contact +list) + +The owner of a conference can make a public conference private by going into the conference +details and hit the settings button (the one with the gears) and select both *private* and +*members only*. + +##### OpenPGP + +Every participant has to announce their OpenPGP key (see answer above). +If you would like to send encrypted messages to a conference you have to make +sure that you have every participant's public key in your OpenKeychain. +Right now there is no check in Conversations to ensurethat. +You have to take care of that yourself. Go to the conference details and touch every key id (The hexadecimal number below a contact). This will send you to OpenKeychain which will assist you on adding the key. This works best in very small conferences with contacts you are already using OpenPGP with. This diff --git a/build.gradle b/build.gradle index deaae0e23..1d5bb8cfe 100644 --- a/build.gradle +++ b/build.gradle @@ -58,8 +58,8 @@ android { defaultConfig { minSdkVersion 14 targetSdkVersion 23 - versionCode 127 - versionName "1.10.0" + versionCode 130 + versionName "1.11.0-beta.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" } diff --git a/proguard-project.txt b/proguard-project.txt deleted file mode 100644 index f2fe1559a..000000000 --- a/proguard-project.txt +++ /dev/null @@ -1,20 +0,0 @@ -# To enable ProGuard in your project, edit project.properties -# to define the proguard.config property as described in that file. -# -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in ${sdk.dir}/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the ProGuard -# include property in project.properties. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/proguard-rules.txt b/proguard-rules.txt deleted file mode 100644 index f39d07c55..000000000 --- a/proguard-rules.txt +++ /dev/null @@ -1,27 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /home/sam/android-sdk-linux/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the ProGuard -# include property in project.properties. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - --dontwarn javax.naming.** - --keep class * extends java.util.ListResourceBundle { - protected Object[][] getContents(); -} - --keepnames class * implements android.os.Parcelable { - public static final ** CREATOR; -} diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 4d64c3aa5..e39cd6549 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -6,13 +6,44 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState; public final class Config { + + private static final int UNENCRYPTED = 1; + private static final int OPENPGP = 2; + private static final int OTR = 4; + private static final int OMEMO = 8; + + private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO; + + public static boolean supportUnencrypted() { + return (ENCRYPTION_MASK & UNENCRYPTED) != 0; + } + + public static boolean supportOpenPgp() { + return (ENCRYPTION_MASK & OPENPGP) != 0; + } + + public static boolean supportOtr() { + return (ENCRYPTION_MASK & OTR) != 0; + } + + public static boolean supportOmemo() { + return (ENCRYPTION_MASK & OMEMO) != 0; + } + + public static boolean multipleEncryptionChoices() { + return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0; + } + public static final String LOGTAG = "conversations"; public static final String DOMAIN_LOCK = null; //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 = false; //set to true to disallow account and settings editing public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox - public static final boolean HIDE_PGP_IN_UI = false; //some more consumer focused clients might want to disable OpenPGP - public static final boolean FORCE_E2E_ENCRYPTION = false; //disables ability to send unencrypted 1-on-1 + public static final boolean ALLOW_NON_TLS_CONNECTIONS = false; //very dangerous. you should have a good reason to set this to true public static final boolean FORCE_ORBOT = false; // always use TOR public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; 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/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index acb4bf1a3..088dfd8ae 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jid.Jid; @@ -58,6 +59,18 @@ public class Bookmark extends Element implements ListItem { } @Override + public String getDisplayJid() { + Jid jid = getJid(); + if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.CONFERENCE_DOMAIN_LOCK)) { + return jid.getLocalpart(); + } else if (jid != null) { + return jid.toString(); + } else { + return null; + } + } + + @Override public Jid getJid() { return this.getAttributeAsJid("jid"); } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 9887d3ef1..691fc3e45 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -121,6 +121,17 @@ public class Contact implements ListItem, Blockable { } } + @Override + public String getDisplayJid() { + if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.DOMAIN_LOCK)) { + return jid.getLocalpart(); + } else if (jid != null) { + return jid.toString(); + } else { + return null; + } + } + public String getProfilePhoto() { return this.photoUri; } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index a179d96bf..0252ea749 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; @@ -48,6 +50,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; @@ -313,6 +316,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; } @@ -659,8 +674,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; @@ -673,12 +688,20 @@ public class Conversation extends AbstractEntity implements Blockable { next = outgoing; } } - if (Config.FORCE_E2E_ENCRYPTION && mode == MODE_SINGLE && next <= 0) { - if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) { + + if (!Config.supportUnencrypted() && next <= 0) { + if (Config.supportOmemo() + && (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) { return Message.ENCRYPTION_AXOLOTL; - } else { + } else if (Config.supportOtr() && mode == MODE_SINGLE) { return Message.ENCRYPTION_OTR; + } 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; } @@ -786,20 +809,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/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java index efc1c2b93..22aedd4b3 100644 --- a/src/main/java/eu/siacs/conversations/entities/ListItem.java +++ b/src/main/java/eu/siacs/conversations/entities/ListItem.java @@ -5,13 +5,15 @@ import java.util.List; import eu.siacs.conversations.xmpp.jid.Jid; public interface ListItem extends Comparable<ListItem> { - public String getDisplayName(); + String getDisplayName(); - public Jid getJid(); + String getDisplayJid(); - public List<Tag> getTags(); + Jid getJid(); - public final class Tag { + List<Tag> getTags(); + + final class Tag { private final String name; private final int color; @@ -29,5 +31,5 @@ public interface ListItem extends Comparable<ListItem> { } } - public boolean match(final String needle); + boolean match(final String needle); } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 3dfbd787c..be9d3d924 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -52,6 +52,7 @@ public class Message extends AbstractEntity { public static final String STATUS = "status"; public static final String TYPE = "type"; public static final String CARBON = "carbon"; + public static final String OOB = "oob"; public static final String EDITED = "edited"; public static final String REMOTE_MSG_ID = "remoteMsgId"; public static final String SERVER_MSG_ID = "serverMsgId"; @@ -72,6 +73,7 @@ public class Message extends AbstractEntity { protected int status; protected int type; protected boolean carbon = false; + protected boolean oob = false; protected String edited = null; protected String relativeFilePath; protected boolean read = true; @@ -107,7 +109,8 @@ public class Message extends AbstractEntity { null, null, true, - null); + null, + false); this.conversation = conversation; } @@ -116,7 +119,7 @@ public class Message extends AbstractEntity { final int encryption, final int status, final int type, final boolean carbon, final String remoteMsgId, final String relativeFilePath, final String serverMsgId, final String fingerprint, final boolean read, - final String edited) { + final String edited, final boolean oob) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -133,6 +136,7 @@ public class Message extends AbstractEntity { this.axolotlFingerprint = fingerprint; this.read = read; this.edited = edited; + this.oob = oob; } public static Message fromCursor(Cursor cursor) { @@ -173,7 +177,8 @@ public class Message extends AbstractEntity { cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), cursor.getString(cursor.getColumnIndex(FINGERPRINT)), cursor.getInt(cursor.getColumnIndex(READ)) > 0, - cursor.getString(cursor.getColumnIndex(EDITED))); + cursor.getString(cursor.getColumnIndex(EDITED)), + cursor.getInt(cursor.getColumnIndex(OOB)) > 0); } public static Message createStatusMessage(Conversation conversation, String body) { @@ -219,6 +224,7 @@ public class Message extends AbstractEntity { values.put(FINGERPRINT, axolotlFingerprint); values.put(READ,read ? 1 : 0); values.put(EDITED, edited); + values.put(OOB, oob ? 1 : 0); return values; } @@ -554,6 +560,10 @@ public class Message extends AbstractEntity { return edited; } + public void setOob(boolean isOob) { + this.oob = isOob; + } + public enum Decision { MUST, SHOULD, @@ -576,7 +586,7 @@ public class Message extends AbstractEntity { if (dotPosition != -1) { String extension = filename.substring(dotPosition + 1); // we want the real file extension, not the crypto one - if (Arrays.asList(Transferable.VALID_CRYPTO_EXTENSIONS).contains(extension)) { + if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) { return extractRelevantExtension(filename.substring(0,dotPosition)); } else { return extension; @@ -610,6 +620,8 @@ public class Message extends AbstractEntity { URL url = new URL(body); if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) { return Decision.NEVER; + } else if (oob) { + return Decision.MUST; } String extension = extractRelevantExtension(url); if (extension == null) { @@ -624,8 +636,8 @@ public class Message extends AbstractEntity { } else { return Decision.NEVER; } - } else if (Arrays.asList(Transferable.VALID_IMAGE_EXTENSIONS).contains(extension) - || Arrays.asList(Transferable.WELL_KNOWN_EXTENSIONS).contains(extension)) { + } else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension) + || Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) { return Decision.SHOULD; } else { return Decision.NEVER; diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 3567c14f4..4fe26743e 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/entities/Transferable.java b/src/main/java/eu/siacs/conversations/entities/Transferable.java index 016c81bdf..5a47c4512 100644 --- a/src/main/java/eu/siacs/conversations/entities/Transferable.java +++ b/src/main/java/eu/siacs/conversations/entities/Transferable.java @@ -1,10 +1,13 @@ package eu.siacs.conversations.entities; +import java.util.Arrays; +import java.util.List; + public interface Transferable { - String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"}; - String[] VALID_CRYPTO_EXTENSIONS = {"pgp", "gpg", "otr"}; - String[] WELL_KNOWN_EXTENSIONS = {"pdf","m4a","mp4"}; + List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe"); + List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg", "otr"); + List<String> WELL_KNOWN_EXTENSIONS = Arrays.asList("pdf","m4a","mp4","3gp","aac","amr","mp3"); int STATUS_UNKNOWN = 0x200; int STATUS_CHECKING = 0x201; diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 4395a5460..a3b0f6506 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -335,4 +335,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/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 455a0b14b..84a44715b 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -89,7 +89,7 @@ public class HttpDownloadConnection implements Transferable { this.message.setEncryption(Message.ENCRYPTION_NONE); } String extension; - if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) { + if (VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { extension = secondToLast; } else { extension = lastPart; diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 43edb2c38..9470dab88 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -3,10 +3,10 @@ package eu.siacs.conversations.parser; import android.util.Log; import android.util.Pair; -import eu.siacs.conversations.crypto.PgpDecryptionService; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; +import java.net.URL; import java.util.ArrayList; import java.util.Set; import java.util.UUID; @@ -104,7 +104,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()); @@ -291,7 +291,9 @@ public class MessageParser extends AbstractParser implements final String body = packet.getBody(); final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); - final Element replaceElement = packet.findChild("replace","urn:xmpp:message-correct:0"); + final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0"); + final Element oob = packet.findChild("x", "jabber:x:oob"); + final boolean isOob = oob!= null && body != null && body.equals(oob.findChildContent("url")); final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id"); final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); int status; @@ -345,7 +347,7 @@ public class MessageParser extends AbstractParser implements } } Message message; - if (body != null && body.startsWith("?OTR")) { + if (body != null && body.startsWith("?OTR") && Config.supportOtr()) { if (!isForwarded && !isTypeGroupChat && isProperlyAddressed) { message = parseOtrChat(body, from, remoteMsgId, conversation); if (message == null) { @@ -355,10 +357,20 @@ public class MessageParser extends AbstractParser implements Log.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring OTR message from "+from+" isForwarded="+Boolean.toString(isForwarded)+", isProperlyAddressed="+Boolean.valueOf(isProperlyAddressed)); message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } - } else if (pgpEncrypted != null) { + } else if (pgpEncrypted != null && Config.supportOpenPgp()) { message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); - } else if (axolotlEncrypted != null) { - message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status); + } else if (axolotlEncrypted != null && Config.supportOmemo()) { + 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; } @@ -375,6 +387,7 @@ public class MessageParser extends AbstractParser implements message.setServerMsgId(serverMsgId); message.setCarbon(isCarbon); message.setTime(timestamp); + message.setOob(isOob); message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); if (conversation.getMode() == Conversation.MODE_MULTI) { Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart()); 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/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index dcba4f748..b975a0017 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -51,7 +51,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 24; + private static final int DATABASE_VERSION = 25; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -163,6 +163,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.CARBON + " INTEGER, " + Message.EDITED + " TEXT, " + Message.READ + " NUMBER DEFAULT 1, " + + Message.OOB + " INTEGER, " + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID @@ -375,6 +376,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (oldVersion < 24 && newVersion >= 24) { db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT"); } + + if (oldVersion < 25 && newVersion >= 25) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER"); + } } public static synchronized DatabaseBackend getInstance(Context context) { 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 ec13d6dc2..ecf5504ba 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); @@ -262,7 +270,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (mPushManagementService.available(account)) { mPushManagementService.registerPushTokenOnServer(account); } - mMessageArchiveService.executePendingQueries(account); connectMultiModeConversations(account); syncDirtyContacts(account); } @@ -276,6 +283,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa mOnAccountUpdate.onAccountUpdate(); } if (account.getStatus() == Account.State.ONLINE) { + mMessageArchiveService.executePendingQueries(account); if (connection != null && connection.getFeatures().csi()) { if (checkListeners()) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//inactive"); @@ -351,7 +359,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public PgpEngine getPgpEngine() { - if (pgpServiceConnection != null && pgpServiceConnection.isBound()) { + if (!Config.supportOpenPgp()) { + return null; + } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) { if (this.mPgpEngine == null) { this.mPgpEngine = new PgpEngine(new OpenPgpApi( getApplicationContext(), @@ -609,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); } @@ -643,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) { @@ -689,20 +707,23 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver); this.fileObserver.startWatching(); - this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() { - @Override - public void onBound(IOpenPgpService2 service) { - for (Account account : accounts) { - if (account.getPgpDecryptionService() != null) { - account.getPgpDecryptionService().onOpenPgpServiceBound(); + if (Config.supportOpenPgp()) { + this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() { + @Override + public void onBound(IOpenPgpService2 service) { + for (Account account : accounts) { + if (account.getPgpDecryptionService() != null) { + account.getPgpDecryptionService().onOpenPgpServiceBound(); + } } } - } - @Override - public void onError(Exception e) { } - }); - this.pgpServiceConnection.bindToService(); + @Override + public void onError(Exception e) { + } + }); + this.pgpServiceConnection.bindToService(); + } this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE); this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService"); @@ -1777,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()); @@ -1788,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 { @@ -1807,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); } @@ -1833,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); @@ -1906,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); @@ -2006,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"); @@ -2025,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) { @@ -2505,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 @@ -2533,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(); } @@ -2624,7 +2673,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public boolean allowMessageCorrection() { - return getPreferences().getBoolean("allow_message_correction", true); + return getPreferences().getBoolean("allow_message_correction", false); } public boolean sendChatStates() { diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index be4549369..adbb09537 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -6,7 +6,6 @@ import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.IntentSender.SendIntentException; -import android.graphics.Bitmap; import android.os.Build; import android.os.Bundle; import android.view.ContextMenu; @@ -334,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 @@ -512,7 +511,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers mAccountJid.setText(getString(R.string.using_account, account)); mYourPhoto.setImageBitmap(avatarService().get(mConversation.getAccount(), getPixel(48))); setTitle(mConversation.getName()); - mFullJid.setText(mConversation.getJid().toBareJid().toString()); + if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && mConversation.getJid().getDomainpart().equals(Config.CONFERENCE_DOMAIN_LOCK)) { + mFullJid.setText(mConversation.getJid().getLocalpart()); + } else { + mFullJid.setText(mConversation.getJid().toBareJid().toString()); + } mYourNick.setText(mucOptions.getActualNick()); mRoleAffiliaton = (TextView) findViewById(R.id.muc_role); if (mucOptions.online()) { diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index d50e0661f..4165b4be5 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -139,7 +139,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd ContactDetailsActivity.this); builder.setTitle(getString(R.string.action_add_phone_book)); builder.setMessage(getString(R.string.add_phone_book_text, - contact.getJid())); + contact.getDisplayJid())); builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton(getString(R.string.add), addToPhonebook); builder.create().show(); @@ -236,7 +236,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd builder.setTitle(getString(R.string.action_delete_contact)) .setMessage( getString(R.string.remove_contact_text, - contact.getJid())) + contact.getDisplayJid())) .setPositiveButton(getString(R.string.delete), removeFromRoster).create().show(); break; @@ -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() { @@ -358,10 +358,10 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } if (contact.getPresences().size() > 1) { - contactJidTv.setText(contact.getJid() + " (" + contactJidTv.setText(contact.getDisplayJid() + " (" + contact.getPresences().size() + ")"); } else { - contactJidTv.setText(contact.getJid().toString()); + contactJidTv.setText(contact.getDisplayJid()); } String account; if (Config.DOMAIN_LOCK != null) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 11e2e8892..eac531242 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -408,9 +408,10 @@ public class ConversationActivity extends XmppActivity menuContactDetails.setVisible(false); menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating()); menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite()); - menuSecure.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION); //if pgp is hidden conferences have no choice of encryption + 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()); } if (this.getSelectedConversation().isMuted()) { menuMute.setVisible(false); @@ -419,7 +420,7 @@ public class ConversationActivity extends XmppActivity } } } - return true; + return super.onCreateOptionsMenu(menu); } protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { @@ -849,13 +850,14 @@ public class ConversationActivity extends XmppActivity MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none); MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp); MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl); - pgp.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION); - none.setVisible(!Config.FORCE_E2E_ENCRYPTION || conversation.getMode() == Conversation.MODE_MULTI); - otr.setVisible(!Config.X509_VERIFICATION); + pgp.setVisible(Config.supportOpenPgp()); + none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI); + otr.setVisible(Config.supportOtr()); + 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()) { @@ -1528,18 +1530,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 15e4b9852..9ef950c48 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -53,6 +53,7 @@ import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; +import eu.siacs.conversations.http.HttpDownloadConnection; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; @@ -123,7 +124,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 +165,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(); @@ -515,6 +521,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private void populateContextMenu(ContextMenu menu) { final Message m = this.selectedMessage; + final Transferable t = m.getTransferable(); Message relevantForCorrection = m; while(relevantForCorrection.mergeable(relevantForCorrection.next())) { relevantForCorrection = relevantForCorrection.next(); @@ -531,7 +538,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa MenuItem downloadFile = menu.findItem(R.id.download_file); MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); if ((m.getType() == Message.TYPE_TEXT || m.getType() == Message.TYPE_PRIVATE) - && m.getTransferable() == null + && t == null && !GeoHelper.isGeoUri(m.getBody()) && m.treatAsDownloadable() != Message.Decision.MUST) { copyText.setVisible(true); @@ -545,7 +552,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } if ((m.getType() != Message.TYPE_TEXT && m.getType() != Message.TYPE_PRIVATE - && m.getTransferable() == null) + && t == null) || (GeoHelper.isGeoUri(m.getBody()))) { shareWith.setVisible(true); } @@ -554,15 +561,16 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } if (m.hasFileOnRemoteHost() || GeoHelper.isGeoUri(m.getBody()) - || m.treatAsDownloadable() == Message.Decision.MUST) { + || m.treatAsDownloadable() == Message.Decision.MUST + || (t != null && t instanceof HttpDownloadConnection)) { copyUrl.setVisible(true); } - if ((m.getType() == Message.TYPE_TEXT && m.getTransferable() == null && m.treatAsDownloadable() != Message.Decision.NEVER) - || (m.isFileOrImage() && m.getTransferable() instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())){ + if ((m.getType() == Message.TYPE_TEXT && t == null && m.treatAsDownloadable() != Message.Decision.NEVER) + || (m.isFileOrImage() && t instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())){ downloadFile.setVisible(true); downloadFile.setTitle(activity.getString(R.string.download_x_file,UIHelper.getFileDescriptionString(activity, m))); } - if ((m.getTransferable() != null && !(m.getTransferable() instanceof TransferablePlaceholder)) + if ((t != null && !(t instanceof TransferablePlaceholder)) || (m.isFileOrImage() && (m.getStatus() == Message.STATUS_WAITING || m.getStatus() == Message.STATUS_OFFERED))) { cancelTransmission.setVisible(true); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 266ae8406..b6f1b430c 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(72))); diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index bb55420d5..a6b3c73c7 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -2,13 +2,12 @@ package eu.siacs.conversations.ui; import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface; import android.view.LayoutInflater; import android.view.View; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Spinner; +import android.widget.TextView; import java.util.List; @@ -44,12 +43,17 @@ public class EnterJidDialog { final String title, final String positiveButton, final String prefilledJid, final String account, boolean allowEditJid ) { + final boolean lock = Config.LOCK_DOMAINS_IN_CONVERSATIONS && Config.DOMAIN_LOCK != null; AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(title); View dialogView = LayoutInflater.from(context).inflate(R.layout.enter_jid_dialog, null); + final TextView jabberIdDesc = (TextView) dialogView.findViewById(R.id.jabber_id); + jabberIdDesc.setText(lock ? R.string.username : R.string.account_settings_jabber_id); final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid); - jid.setAdapter(new KnownHostsAdapter(context,android.R.layout.simple_list_item_1, knownHosts)); + if (!lock) { + jid.setAdapter(new KnownHostsAdapter(context, android.R.layout.simple_list_item_1, knownHosts)); + } if (prefilledJid != null) { jid.append(prefilledJid); if (!allowEditJid) { @@ -60,6 +64,7 @@ public class EnterJidDialog { } } + jid.setHint(Config.LOCK_DOMAINS_IN_CONVERSATIONS && Config.DOMAIN_LOCK != null ? R.string.username_hint : R.string.account_settings_example_jabber_id); if (account == null) { StartConversationActivity.populateAccountSpinner(context, activatedAccounts, spinner); @@ -95,9 +100,13 @@ public class EnterJidDialog { } final Jid contactJid; try { - contactJid = Jid.fromString(jid.getText().toString()); + if (lock) { + contactJid = Jid.fromParts(jid.getText().toString(), Config.DOMAIN_LOCK, null); + } else { + contactJid = Jid.fromString(jid.getText().toString()); + } } catch (final InvalidJidException e) { - jid.setError(context.getString(R.string.invalid_jid)); + jid.setError(context.getString(lock ? R.string.invalid_username : R.string.invalid_jid)); return; } 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 f2511ecb0..feac2c622 100644 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -123,7 +123,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false); } else { menu.findItem(R.id.mgmt_account_enable).setVisible(false); - menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(!Config.HIDE_PGP_IN_UI); + menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp()); } menu.setHeaderTitle(this.selectedAccount.getJid().toBareJid().toString()); } @@ -154,7 +154,10 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda if (Config.X509_VERIFICATION) { addAccount.setVisible(false); addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + } else { + addAccount.setVisible(!Config.LOCK_SETTINGS); } + addAccountWithCertificate.setVisible(!Config.LOCK_SETTINGS); if (!accountsLeftToEnable()) { enableAll.setVisible(false); diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index a457bf966..ee70ee43b 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -7,6 +7,8 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -27,13 +29,12 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.utils.ExifHelper; import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; public class PublishProfilePictureActivity extends XmppActivity { - private static final int REQUEST_CHOOSE_FILE = 0xac23; + private static final int REQUEST_CHOOSE_FILE_AND_CROP = 0xac23; + private static final int REQUEST_CHOOSE_FILE = 0xac24; private ImageView avatar; private TextView accountTextView; private TextView hintOrWarning; @@ -138,7 +139,7 @@ public class PublishProfilePictureActivity extends XmppActivity { @Override public void onClick(View v) { if (hasStoragePermission(REQUEST_CHOOSE_FILE)) { - chooseAvatar(); + chooseAvatar(false); } } @@ -146,20 +147,22 @@ public class PublishProfilePictureActivity extends XmppActivity { this.defaultUri = PhoneHelper.getSefliUri(getApplicationContext()); } - private void chooseAvatar() { + private void chooseAvatar(boolean crop) { Intent attachFileIntent = new Intent(); attachFileIntent.setType("image/*"); attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file)); - startActivityForResult(chooser, REQUEST_CHOOSE_FILE); + startActivityForResult(chooser, crop ? REQUEST_CHOOSE_FILE_AND_CROP : REQUEST_CHOOSE_FILE); } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - if (requestCode == REQUEST_CHOOSE_FILE) { - chooseAvatar(); + if (requestCode == REQUEST_CHOOSE_FILE_AND_CROP) { + chooseAvatar(true); + } else if (requestCode == REQUEST_CHOOSE_FILE) { + chooseAvatar(false); } } else { Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); @@ -167,11 +170,29 @@ public class PublishProfilePictureActivity extends XmppActivity { } @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.publish_avatar, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == R.id.action_crop_image) { + if (hasStoragePermission(REQUEST_CHOOSE_FILE_AND_CROP)) { + chooseAvatar(true); + } + return true; + } else { + return onOptionsItemSelected(item); + } + } + + @Override protected void onActivityResult(int requestCode, int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { switch (requestCode) { - case REQUEST_CHOOSE_FILE: + case REQUEST_CHOOSE_FILE_AND_CROP: Uri source = data.getData(); String original = FileUtils.getPath(this, source); if (original != null) { @@ -181,9 +202,17 @@ public class PublishProfilePictureActivity extends XmppActivity { final int size = getPixel(192); Crop.of(source, destination).asSquare().withMaxSize(size, size).start(this); break; + case REQUEST_CHOOSE_FILE: + this.avatarUri = data.getData(); + if (xmppConnectionServiceBound) { + loadImageIntoPreview(this.avatarUri); + } + break; case Crop.REQUEST_CROP: this.avatarUri = Uri.fromFile(new File(getCacheDir(), "croppedAvatar")); - loadImageIntoPreview(this.avatarUri); + if (xmppConnectionServiceBound) { + loadImageIntoPreview(this.avatarUri); + } break; } } else { @@ -248,21 +277,10 @@ public class PublishProfilePictureActivity extends XmppActivity { } } - private Bitmap loadScaledBitmap(Uri uri, int reqSize) throws FileNotFoundException { - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, options); - int rotation = ExifHelper.getOrientation(getContentResolver().openInputStream(uri)); - options.inSampleSize = FileBackend.calcSampleSize(options, reqSize); - options.inJustDecodeBounds = false; - Bitmap bm = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, options); - return FileBackend.rotate(bm,rotation); - } - protected void loadImageIntoPreview(Uri uri) { Bitmap bm = null; try { - bm = loadScaledBitmap(uri, getPixel(192)); + bm = xmppConnectionService.getFileBackend().cropCenterSquare(uri, getPixel(192)); } catch (Exception e) { e.printStackTrace(); } 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 8b3b4607b..7d650e5bb 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -44,6 +44,7 @@ import android.widget.Checkable; import android.widget.EditText; import android.widget.ListView; import android.widget.Spinner; +import android.widget.TextView; import android.widget.Toast; import com.google.zxing.integration.android.IntentIntegrator; @@ -64,7 +65,6 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.adapter.ListItemAdapter; @@ -390,7 +390,13 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU final View dialogView = getLayoutInflater().inflate(R.layout.join_conference_dialog, null); final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid); - jid.setAdapter(new KnownHostsAdapter(this, android.R.layout.simple_list_item_1, mKnownConferenceHosts)); + final boolean lock = Config.LOCK_DOMAINS_IN_CONVERSATIONS && Config.CONFERENCE_DOMAIN_LOCK != null; + final TextView jabberIdDesc = (TextView) dialogView.findViewById(R.id.jabber_id); + jabberIdDesc.setText(lock ? R.string.conference_name : R.string.conference_address); + jid.setHint(lock ? R.string.conference_name : R.string.conference_address_example); + if (!lock) { + jid.setAdapter(new KnownHostsAdapter(this, android.R.layout.simple_list_item_1, mKnownConferenceHosts)); + } if (prefilledJid != null) { jid.append(prefilledJid); } @@ -416,9 +422,13 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } final Jid conferenceJid; try { - conferenceJid = Jid.fromString(jid.getText().toString()); + if (lock) { + conferenceJid = Jid.fromParts(jid.getText().toString(),Config.CONFERENCE_DOMAIN_LOCK, null); + } else { + conferenceJid = Jid.fromString(jid.getText().toString()); + } } catch (final InvalidJidException e) { - jid.setError(getString(R.string.invalid_jid)); + jid.setError(getString(lock ? R.string.invalid_conference_name : R.string.invalid_jid)); return; } @@ -523,7 +533,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU mSearchEditText.append(mInitialJid); filter(mInitialJid); } - return true; + return super.onCreateOptionsMenu(menu); } @Override @@ -730,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 003b802c5..5682f7853 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; @@ -377,6 +378,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); @@ -785,7 +799,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/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index 47414f900..da8e39101 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -76,10 +76,10 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { tagLayout.addView(tv); } } - final Jid jid = item.getJid(); + final String jid = item.getDisplayJid(); if (jid != null) { tvJid.setVisibility(View.VISIBLE); - tvJid.setText(jid.toString()); + tvJid.setText(jid); } else { tvJid.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 9339809c2..ab9f74a36 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -32,6 +32,7 @@ import android.widget.TextView; import android.widget.Toast; import java.lang.ref.WeakReference; +import java.net.URL; import java.util.List; import java.util.concurrent.RejectedExecutionException; import java.util.regex.Matcher; @@ -635,7 +636,19 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else if (message.bodyIsHeart()) { displayHeartMessage(viewHolder, message.getBody().trim()); } else if (message.treatAsDownloadable() == Message.Decision.MUST) { - displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message))); + try { + URL url = new URL(message.getBody()); + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize_on_host, + UIHelper.getFileDescriptionString(activity, message), + url.getHost())); + } catch (Exception e) { + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize, + UIHelper.getFileDescriptionString(activity, message))); + } } else { displayTextMessage(viewHolder, message, darkBackground); } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 5e8af085c..8cae81171 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -187,10 +187,13 @@ public class UIHelper { UIHelper.getMessageDisplayName(message) + " "), false); } else if (GeoHelper.isGeoUri(message.getBody())) { if (message.getStatus() == Message.STATUS_RECEIVED) { - return new Pair<>(context.getString(R.string.received_location),true); + return new Pair<>(context.getString(R.string.received_location), true); } else { return new Pair<>(context.getString(R.string.location), true); } + } else if (message.treatAsDownloadable() == Message.Decision.MUST) { + return new Pair<>(context.getString(R.string.x_file_offered_for_download, + getFileDescriptionString(context,message)),true); } else{ return new Pair<>(message.getBody().trim(), false); } 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..dcb13c233 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) { @@ -322,14 +322,14 @@ public class JingleConnection implements Transferable { String[] filename = fileNameElement.getContent() .toLowerCase(Locale.US).toLowerCase().split("\\."); String extension = filename[filename.length - 1]; - if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(extension)) { + if (VALID_IMAGE_EXTENSIONS.contains(extension)) { message.setType(Message.TYPE_IMAGE); message.setRelativeFilePath(message.getUuid()+"."+extension); - } else if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains( + } else if (VALID_CRYPTO_EXTENSIONS.contains( filename[filename.length - 1])) { if (filename.length == 3) { extension = filename[filename.length - 2]; - if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(extension)) { + if (VALID_IMAGE_EXTENSIONS.contains(extension)) { message.setType(Message.TYPE_IMAGE); message.setRelativeFilePath(message.getUuid()+"."+extension); } else { diff --git a/src/main/res/drawable-hdpi/ic_crop_white_24dp.png b/src/main/res/drawable-hdpi/ic_crop_white_24dp.png Binary files differnew file mode 100644 index 000000000..6cbb87ce8 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_crop_white_24dp.png diff --git a/src/main/res/drawable-mdpi/ic_crop_white_24dp.png b/src/main/res/drawable-mdpi/ic_crop_white_24dp.png Binary files differnew file mode 100644 index 000000000..d606c0725 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_crop_white_24dp.png diff --git a/src/main/res/drawable-xhdpi/ic_crop_white_24dp.png b/src/main/res/drawable-xhdpi/ic_crop_white_24dp.png Binary files differnew file mode 100644 index 000000000..983af77f1 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_crop_white_24dp.png diff --git a/src/main/res/drawable-xxhdpi/ic_crop_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_crop_white_24dp.png Binary files differnew file mode 100644 index 000000000..8c9eecb9c --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_crop_white_24dp.png diff --git a/src/main/res/drawable-xxxhdpi/ic_crop_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_crop_white_24dp.png Binary files differnew file mode 100644 index 000000000..35d5434af --- /dev/null +++ b/src/main/res/drawable-xxxhdpi/ic_crop_white_24dp.png 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/menu/publish_avatar.xml b/src/main/res/menu/publish_avatar.xml new file mode 100644 index 000000000..1bffb2961 --- /dev/null +++ b/src/main/res/menu/publish_avatar.xml @@ -0,0 +1,8 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/action_crop_image" + android:showAsAction="always" + android:icon="@drawable/ic_crop_white_24dp" + android:title="@string/select_image_and_crop"/> +</menu>
\ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 8e3629ca2..9657ea245 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -344,6 +344,7 @@ <string name="checking_x">Checking %s on HTTP host</string> <string name="not_connected_try_again">You are not connected. Try again later</string> <string name="check_x_filesize">Check %s size</string> + <string name="check_x_filesize_on_host">Check %1$s size on %2$s</string> <string name="message_options">Message options</string> <string name="copy_text">Copy text</string> <string name="copy_original_url">Copy original URL</string> @@ -522,6 +523,8 @@ <string name="username">Username</string> <string name="username_hint">Username</string> <string name="invalid_username">This is not a valid username</string> + <string name="conference_name">Conference name</string> + <string name="invalid_conference_name">This is not a valid conference name</string> <string name="download_failed_server_not_found">Download failed: Server not found</string> <string name="download_failed_file_not_found">Download failed: File not found</string> <string name="download_failed_could_not_connect">Download failed: Could not connect to host</string> @@ -534,6 +537,8 @@ <string name="pref_away_when_screen_off_summary">Marks your resource as away when the screen is turned off</string> <string name="pref_xa_on_silent_mode">Not available in silent mode</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> @@ -597,4 +602,6 @@ <string name="this_field_is_required">This field is required</string> <string name="correct_message">Correct message</string> <string name="send_corrected_message">Send corrected message</string> + <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> + <string name="select_image_and_crop">Select image and crop</string> </resources> diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index e8747a8bb..f505baf7c 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -147,7 +147,7 @@ android:summary="@string/pref_remove_trusted_certificates_summary" android:title="@string/pref_remove_trusted_certificates_title"/> <CheckBoxPreference - android:defaultValue="true" + android:defaultValue="false" android:key="allow_message_correction" android:title="@string/pref_allow_message_correction" android:summary="@string/pref_allow_message_correction_summary"/> @@ -189,6 +189,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 |