aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java14
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/OtrService.java31
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/PgpEngine.java15
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java728
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java7
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java4
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java5
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java421
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java249
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java196
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java3
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Account.java12
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Contact.java141
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Conversation.java76
-rw-r--r--src/main/java/eu/siacs/conversations/entities/DownloadableFile.java130
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java113
-rw-r--r--src/main/java/eu/siacs/conversations/entities/MucOptions.java13
-rw-r--r--src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java4
-rw-r--r--src/main/java/eu/siacs/conversations/generator/IqGenerator.java65
-rw-r--r--src/main/java/eu/siacs/conversations/generator/MessageGenerator.java54
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java4
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java94
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java68
-rw-r--r--src/main/java/eu/siacs/conversations/parser/AbstractParser.java8
-rw-r--r--src/main/java/eu/siacs/conversations/parser/IqParser.java175
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java73
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java519
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/FileBackend.java106
-rw-r--r--src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java101
-rw-r--r--src/main/java/eu/siacs/conversations/services/EventReceiver.java3
-rw-r--r--src/main/java/eu/siacs/conversations/services/MessageArchiveService.java2
-rw-r--r--src/main/java/eu/siacs/conversations/services/NotificationService.java7
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java269
-rw-r--r--src/main/java/eu/siacs/conversations/ui/AboutPreference.java1
-rw-r--r--src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java15
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java5
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java10
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java47
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java42
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationActivity.java248
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java178
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java262
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java19
-rw-r--r--src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java13
-rw-r--r--src/main/java/eu/siacs/conversations/ui/SettingsActivity.java19
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java5
-rw-r--r--src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java27
-rw-r--r--src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java265
-rw-r--r--src/main/java/eu/siacs/conversations/ui/XmppActivity.java163
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java24
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java6
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java8
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java104
-rw-r--r--src/main/java/eu/siacs/conversations/ui/widget/Switch.java68
-rw-r--r--src/main/java/eu/siacs/conversations/utils/CryptoHelper.java9
-rw-r--r--src/main/java/eu/siacs/conversations/utils/DNSHelper.java44
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java4
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java24
-rw-r--r--src/main/java/eu/siacs/conversations/utils/FileUtils.java144
-rw-r--r--src/main/java/eu/siacs/conversations/utils/MimeUtils.java2
-rw-r--r--src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java4
-rw-r--r--src/main/java/eu/siacs/conversations/utils/PhoneHelper.java9
-rw-r--r--src/main/java/eu/siacs/conversations/utils/UIHelper.java20
-rw-r--r--src/main/java/eu/siacs/conversations/xml/Element.java10
-rw-r--r--src/main/java/eu/siacs/conversations/xml/XmlReader.java16
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java5
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java251
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java158
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java7
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java18
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java43
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java26
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java5
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java4
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java7
76 files changed, 4967 insertions, 1090 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 00225f7b..f2303626 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -8,6 +8,11 @@ public final class Config {
public static final String LOGTAG = "conversations";
+
+ public static final String DOMAIN_LOCK = null; //only allow account creation for this domain
+ 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 int PING_MAX_INTERVAL = 300;
public static final int PING_MIN_INTERVAL = 30;
public static final int PING_TIMEOUT = 10;
@@ -19,6 +24,10 @@ public final class Config {
public static final int AVATAR_SIZE = 192;
public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
+ public static final int IMAGE_SIZE = 1920;
+ public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG;
+ public static final int IMAGE_QUALITY = 75;
+
public static final int MESSAGE_MERGE_WINDOW = 20;
public static final boolean UTF8_EMOTICONS = false;
@@ -26,7 +35,6 @@ public final class Config {
public static final int PAGE_SIZE = 50;
public static final int MAX_NUM_PAGES = 3;
- public static final int PROGRESS_UI_UPDATE_INTERVAL = 750;
public static final int REFRESH_UI_INTERVAL = 500;
public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb
@@ -36,6 +44,10 @@ public final class Config {
public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false;
+ public static final boolean REPORT_WRONG_FILESIZE_IN_OTR_JINGLE = true;
+
+ public static final boolean SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON = false;
+
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2;
public static final int MAM_MAX_MESSAGES = 500;
diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrService.java b/src/main/java/eu/siacs/conversations/crypto/OtrService.java
index c23a7fc8..1b296cc4 100644
--- a/src/main/java/eu/siacs/conversations/crypto/OtrService.java
+++ b/src/main/java/eu/siacs/conversations/crypto/OtrService.java
@@ -1,5 +1,20 @@
package eu.siacs.conversations.crypto;
+import android.util.Log;
+
+import net.java.otr4j.OtrEngineHost;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.OtrPolicyImpl;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.FragmenterInstructions;
+import net.java.otr4j.session.InstanceTag;
+import net.java.otr4j.session.SessionID;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
@@ -11,31 +26,15 @@ import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.util.Log;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
-import net.java.otr4j.OtrEngineHost;
-import net.java.otr4j.OtrException;
-import net.java.otr4j.OtrPolicy;
-import net.java.otr4j.OtrPolicyImpl;
-import net.java.otr4j.crypto.OtrCryptoEngineImpl;
-import net.java.otr4j.crypto.OtrCryptoException;
-import net.java.otr4j.session.InstanceTag;
-import net.java.otr4j.session.SessionID;
-import net.java.otr4j.session.FragmenterInstructions;
-
public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
private Account account;
diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
index f393264c..33089a8c 100644
--- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
+++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
@@ -1,5 +1,13 @@
package eu.siacs.conversations.crypto;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.Uri;
+
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
+
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
@@ -9,10 +17,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
-import org.openintents.openpgp.OpenPgpSignatureResult;
-import org.openintents.openpgp.util.OpenPgpApi;
-import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
-
import de.tzur.conversations.Settings;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@@ -23,9 +27,6 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.net.Uri;
public class PgpEngine {
private OpenPgpApi api;
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
new file mode 100644
index 00000000..d700b644
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
@@ -0,0 +1,728 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.IdentityKeyPair;
+import org.whispersystems.libaxolotl.InvalidKeyException;
+import org.whispersystems.libaxolotl.InvalidKeyIdException;
+import org.whispersystems.libaxolotl.SessionBuilder;
+import org.whispersystems.libaxolotl.UntrustedIdentityException;
+import org.whispersystems.libaxolotl.ecc.ECPublicKey;
+import org.whispersystems.libaxolotl.state.PreKeyBundle;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+import org.whispersystems.libaxolotl.util.KeyHelper;
+
+import java.security.Security;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.parser.IqParser;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+public class AxolotlService {
+
+ public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
+ public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
+ public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
+
+ public static final String LOGPREFIX = "AxolotlService";
+
+ public static final int NUM_KEYS_TO_PUBLISH = 100;
+
+ private final Account account;
+ private final XmppConnectionService mXmppConnectionService;
+ private final SQLiteAxolotlStore axolotlStore;
+ private final SessionMap sessions;
+ private final Map<Jid, Set<Integer>> deviceIds;
+ private final Map<String, XmppAxolotlMessage> messageCache;
+ private final FetchStatusMap fetchStatusMap;
+ private final SerialSingleThreadExecutor executor;
+
+ private static class AxolotlAddressMap<T> {
+ protected Map<String, Map<Integer, T>> map;
+ protected final Object MAP_LOCK = new Object();
+
+ public AxolotlAddressMap() {
+ this.map = new HashMap<>();
+ }
+
+ public void put(AxolotlAddress address, T value) {
+ synchronized (MAP_LOCK) {
+ Map<Integer, T> devices = map.get(address.getName());
+ if (devices == null) {
+ devices = new HashMap<>();
+ map.put(address.getName(), devices);
+ }
+ devices.put(address.getDeviceId(), value);
+ }
+ }
+
+ public T get(AxolotlAddress address) {
+ synchronized (MAP_LOCK) {
+ Map<Integer, T> devices = map.get(address.getName());
+ if (devices == null) {
+ return null;
+ }
+ return devices.get(address.getDeviceId());
+ }
+ }
+
+ public Map<Integer, T> getAll(AxolotlAddress address) {
+ synchronized (MAP_LOCK) {
+ Map<Integer, T> devices = map.get(address.getName());
+ if (devices == null) {
+ return new HashMap<>();
+ }
+ return devices;
+ }
+ }
+
+ public boolean hasAny(AxolotlAddress address) {
+ synchronized (MAP_LOCK) {
+ Map<Integer, T> devices = map.get(address.getName());
+ return devices != null && !devices.isEmpty();
+ }
+ }
+
+ public void clear() {
+ map.clear();
+ }
+
+ }
+
+ private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
+ private final XmppConnectionService xmppConnectionService;
+ private final Account account;
+
+ public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
+ super();
+ this.xmppConnectionService = service;
+ this.account = account;
+ this.fillMap(store);
+ }
+
+ private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
+ for (Integer deviceId : deviceIds) {
+ AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
+ String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", "");
+ this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint));
+ }
+ }
+
+ private void fillMap(SQLiteAxolotlStore store) {
+ List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString());
+ putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
+ for (Contact contact : account.getRoster().getContacts()) {
+ Jid bareJid = contact.getJid().toBareJid();
+ if (bareJid == null) {
+ continue; // FIXME: handle this?
+ }
+ String address = bareJid.toString();
+ deviceIds = store.getSubDeviceSessions(address);
+ putDevicesForJid(address, deviceIds, store);
+ }
+
+ }
+
+ @Override
+ public void put(AxolotlAddress address, XmppAxolotlSession value) {
+ super.put(address, value);
+ value.setNotFresh();
+ xmppConnectionService.syncRosterToDisk(account);
+ }
+
+ public void put(XmppAxolotlSession session) {
+ this.put(session.getRemoteAddress(), session);
+ }
+ }
+
+ private static enum FetchStatus {
+ PENDING,
+ SUCCESS,
+ ERROR
+ }
+
+ private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
+
+ }
+
+ public static String getLogprefix(Account account) {
+ return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): ";
+ }
+
+ public AxolotlService(Account account, XmppConnectionService connectionService) {
+ if (Security.getProvider("BC") == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ this.mXmppConnectionService = connectionService;
+ this.account = account;
+ this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
+ this.deviceIds = new HashMap<>();
+ this.messageCache = new HashMap<>();
+ this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
+ this.fetchStatusMap = new FetchStatusMap();
+ this.executor = new SerialSingleThreadExecutor();
+ }
+
+ public IdentityKey getOwnPublicKey() {
+ return axolotlStore.getIdentityKeyPair().getPublicKey();
+ }
+
+ public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
+ 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 long getNumTrustedKeys(Contact contact) {
+ return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString());
+ }
+
+ private AxolotlAddress getAddressForJid(Jid jid) {
+ return new AxolotlAddress(jid.toString(), 0);
+ }
+
+ private Set<XmppAxolotlSession> findOwnSessions() {
+ AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
+ Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
+ return ownDeviceSessions;
+ }
+
+ private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
+ AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
+ Set<XmppAxolotlSession> sessions = new HashSet<>(this.sessions.getAll(contactAddress).values());
+ return sessions;
+ }
+
+ private boolean hasAny(Contact contact) {
+ AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
+ return sessions.hasAny(contactAddress);
+ }
+
+ public void regenerateKeys() {
+ axolotlStore.regenerate();
+ sessions.clear();
+ fetchStatusMap.clear();
+ publishBundlesIfNeeded();
+ publishOwnDeviceIdIfNeeded();
+ }
+
+ public int getOwnDeviceId() {
+ return axolotlStore.getLocalRegistrationId();
+ }
+
+ public Set<Integer> getOwnDeviceIds() {
+ return this.deviceIds.get(account.getJid().toBareJid());
+ }
+
+ private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
+ final XmppAxolotlSession.Trust from,
+ final XmppAxolotlSession.Trust to) {
+ for (Integer deviceId : deviceIds) {
+ AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
+ XmppAxolotlSession session = sessions.get(address);
+ if (session != null && session.getFingerprint() != null
+ && session.getTrust() == from) {
+ session.setTrust(to);
+ }
+ }
+ }
+
+ public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
+ if (jid.toBareJid().equals(account.getJid().toBareJid())) {
+ if (deviceIds.contains(getOwnDeviceId())) {
+ deviceIds.remove(getOwnDeviceId());
+ }
+ for (Integer deviceId : deviceIds) {
+ AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
+ if (sessions.get(ownDeviceAddress) == null) {
+ buildSessionFromPEP(ownDeviceAddress);
+ }
+ }
+ }
+ Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
+ expiredDevices.removeAll(deviceIds);
+ setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
+ XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
+ setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
+ XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
+ setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
+ XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
+ Set<Integer> newDevices = new HashSet<>(deviceIds);
+ setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
+ XmppAxolotlSession.Trust.TRUSTED);
+ setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
+ XmppAxolotlSession.Trust.UNDECIDED);
+ setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
+ XmppAxolotlSession.Trust.UNTRUSTED);
+ this.deviceIds.put(jid, deviceIds);
+ mXmppConnectionService.keyStatusUpdated();
+ publishOwnDeviceIdIfNeeded();
+ }
+
+ public void wipeOtherPepDevices() {
+ Set<Integer> deviceIds = new HashSet<>();
+ deviceIds.add(getOwnDeviceId());
+ IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
+ mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ // TODO: implement this!
+ }
+ });
+ }
+
+ public void purgeKey(IdentityKey identityKey) {
+ axolotlStore.setFingerprintTrust(identityKey.getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
+ }
+
+ public void publishOwnDeviceIdIfNeeded() {
+ IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
+ mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Element item = mXmppConnectionService.getIqParser().getItem(packet);
+ Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+ if (deviceIds == null) {
+ deviceIds = new HashSet<Integer>();
+ }
+ if (!deviceIds.contains(getOwnDeviceId())) {
+ deviceIds.add(getOwnDeviceId());
+ IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish);
+ mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ // TODO: implement this!
+ }
+ });
+ }
+ } else {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing device ID:" + packet.findChild("error"));
+ }
+ }
+ });
+ }
+
+ public void publishBundlesIfNeeded() {
+ IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
+ mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
+ Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
+ boolean flush = false;
+ if (bundle == null) {
+ Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet);
+ bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
+ flush = true;
+ }
+ if (keys == null) {
+ Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet);
+ }
+ try {
+ boolean changed = false;
+ // Validate IdentityKey
+ IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
+ if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
+ changed = true;
+ }
+
+ // Validate signedPreKeyRecord + ID
+ SignedPreKeyRecord signedPreKeyRecord;
+ int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
+ try {
+ signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
+ if (flush
+ || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
+ || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
+ signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
+ axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
+ changed = true;
+ }
+ } catch (InvalidKeyIdException e) {
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
+ signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
+ axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
+ changed = true;
+ }
+
+ // Validate PreKeys
+ Set<PreKeyRecord> preKeyRecords = new HashSet<>();
+ if (keys != null) {
+ for (Integer id : keys.keySet()) {
+ try {
+ PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
+ if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
+ preKeyRecords.add(preKeyRecord);
+ }
+ } catch (InvalidKeyIdException ignored) {
+ }
+ }
+ }
+ int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
+ if (newKeys > 0) {
+ List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
+ axolotlStore.getCurrentPreKeyId() + 1, newKeys);
+ preKeyRecords.addAll(newRecords);
+ for (PreKeyRecord record : newRecords) {
+ axolotlStore.storePreKey(record.getId(), record);
+ }
+ changed = true;
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
+ }
+
+
+ if (changed) {
+ IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
+ signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
+ preKeyRecords, getOwnDeviceId());
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
+ mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ // TODO: implement this!
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Published bundle, got: " + packet);
+ }
+ });
+ }
+ } catch (InvalidKeyException e) {
+ Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
+ return;
+ }
+ } else {
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing Bundle:" + packet.findChild("error"));
+ }
+ }
+ });
+ }
+
+ public boolean isContactAxolotlCapable(Contact contact) {
+
+ Jid jid = contact.getJid().toBareJid();
+ AxolotlAddress address = new AxolotlAddress(jid.toString(), 0);
+ return sessions.hasAny(address) ||
+ (deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
+ }
+
+ public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
+ return axolotlStore.getFingerprintTrust(fingerprint);
+ }
+
+ public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
+ axolotlStore.setFingerprintTrust(fingerprint, trust);
+ }
+
+ private void buildSessionFromPEP(final AxolotlAddress address) {
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId());
+
+ try {
+ IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
+ Jid.fromString(address.getName()), address.getDeviceId());
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket);
+ mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
+ private void finish() {
+ AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
+ if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
+ && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
+ mXmppConnectionService.keyStatusUpdated();
+ }
+ }
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
+ final IqParser parser = mXmppConnectionService.getIqParser();
+ final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
+ final PreKeyBundle bundle = parser.bundle(packet);
+ if (preKeyBundleList.isEmpty() || bundle == null) {
+ Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
+ fetchStatusMap.put(address, FetchStatus.ERROR);
+ finish();
+ return;
+ }
+ Random random = new Random();
+ final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
+ if (preKey == null) {
+ //should never happen
+ fetchStatusMap.put(address, FetchStatus.ERROR);
+ finish();
+ return;
+ }
+
+ final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
+ preKey.getPreKeyId(), preKey.getPreKey(),
+ bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
+ bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
+
+ axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey());
+
+ try {
+ SessionBuilder builder = new SessionBuilder(axolotlStore, address);
+ builder.process(preKeyBundle);
+ XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", ""));
+ sessions.put(address, session);
+ fetchStatusMap.put(address, FetchStatus.SUCCESS);
+ } catch (UntrustedIdentityException | InvalidKeyException e) {
+ Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
+ + e.getClass().getName() + ", " + e.getMessage());
+ fetchStatusMap.put(address, FetchStatus.ERROR);
+ }
+
+ finish();
+ } else {
+ fetchStatusMap.put(address, FetchStatus.ERROR);
+ Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error"));
+ finish();
+ return;
+ }
+ }
+ });
+ } catch (InvalidJidException e) {
+ Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName());
+ }
+ }
+
+ public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid());
+ Jid contactJid = conversation.getContact().getJid().toBareJid();
+ 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.getFingerprint().replaceAll("\\s", ""));
+ sessions.put(address, session);
+ } else {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId);
+ addresses.add(new AxolotlAddress(contactJid.toString(), foreignId));
+ }
+ }
+ }
+ } 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())) {
+ AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
+ 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.getFingerprint().replaceAll("\\s", ""));
+ sessions.put(address, session);
+ } else {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId);
+ addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId));
+ }
+ }
+ }
+ }
+
+ return addresses;
+ }
+
+ public boolean createSessionsIfNeeded(final Conversation conversation) {
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed...");
+ boolean newSessions = false;
+ Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
+ for (AxolotlAddress address : addresses) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString());
+ FetchStatus status = fetchStatusMap.get(address);
+ if (status == null || status == FetchStatus.ERROR) {
+ fetchStatusMap.put(address, FetchStatus.PENDING);
+ this.buildSessionFromPEP(address);
+ newSessions = true;
+ } else if (status == FetchStatus.PENDING) {
+ newSessions = true;
+ } else {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString());
+ }
+ }
+
+ return newSessions;
+ }
+
+ public boolean hasPendingKeyFetches(Conversation conversation) {
+ AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
+ AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(), 0);
+ return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
+ || fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
+
+ }
+
+ @Nullable
+ private XmppAxolotlMessage buildHeader(Contact contact) {
+ final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
+ contact.getJid().toBareJid(), getOwnDeviceId());
+
+ Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
+ Set<XmppAxolotlSession> ownSessions = findOwnSessions();
+ if (contactSessions.isEmpty()) {
+ return null;
+ }
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
+ for (XmppAxolotlSession session : contactSessions) {
+ Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
+ axolotlMessage.addDevice(session);
+ }
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
+ for (XmppAxolotlSession session : ownSessions) {
+ Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
+ axolotlMessage.addDevice(session);
+ }
+
+ return axolotlMessage;
+ }
+
+ @Nullable
+ public XmppAxolotlMessage encrypt(Message message) {
+ XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
+
+ if (axolotlMessage != null) {
+ final String content;
+ if (message.hasFileOnRemoteHost()) {
+ content = message.getFileParams().url.toString();
+ } else {
+ content = message.getBody();
+ }
+ try {
+ axolotlMessage.encrypt(content);
+ } catch (CryptoFailedException e) {
+ Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
+ return null;
+ }
+ }
+
+ return axolotlMessage;
+ }
+
+ public void preparePayloadMessage(final Message message, final boolean delay) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ XmppAxolotlMessage axolotlMessage = encrypt(message);
+ if (axolotlMessage == null) {
+ mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
+ //mXmppConnectionService.updateConversationUi();
+ } else {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid());
+ messageCache.put(message.getUuid(), axolotlMessage);
+ mXmppConnectionService.resendMessage(message, delay);
+ }
+ }
+ });
+ }
+
+ public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ XmppAxolotlMessage axolotlMessage = buildHeader(contact);
+ onMessageCreatedCallback.run(axolotlMessage);
+ }
+ });
+ }
+
+ public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
+ XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid());
+ if (axolotlMessage != null) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid());
+ messageCache.remove(message.getUuid());
+ } else {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid());
+ }
+ return axolotlMessage;
+ }
+
+ private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) {
+ IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
+ return (identityKey != null)
+ ? new XmppAxolotlSession(account, axolotlStore, address,
+ identityKey.getFingerprint().replaceAll("\\s", ""))
+ : null;
+ }
+
+ private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
+ AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
+ message.getSenderDeviceId());
+ XmppAxolotlSession session = sessions.get(senderAddress);
+ if (session == null) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
+ session = recreateUncachedSession(senderAddress);
+ if (session == null) {
+ session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
+ }
+ }
+ return session;
+ }
+
+ public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
+ XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
+
+ XmppAxolotlSession session = getReceivingSession(message);
+ try {
+ plaintextMessage = message.decrypt(session, getOwnDeviceId());
+ Integer preKeyId = session.getPreKeyId();
+ if (preKeyId != null) {
+ publishBundlesIfNeeded();
+ session.resetPreKeyId();
+ }
+ } catch (CryptoFailedException e) {
+ Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
+ }
+
+ if (session.isFresh() && plaintextMessage != null) {
+ sessions.put(session);
+ }
+
+ return plaintextMessage;
+ }
+
+ public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
+ XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage = null;
+
+ XmppAxolotlSession session = getReceivingSession(message);
+ keyTransportMessage = message.getParameters(session, getOwnDeviceId());
+
+ if (session.isFresh() && keyTransportMessage != null) {
+ sessions.put(session);
+ }
+
+ return keyTransportMessage;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java
new file mode 100644
index 00000000..5796ef30
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+public class CryptoFailedException extends Exception {
+ public CryptoFailedException(Exception e){
+ super(e);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java
new file mode 100644
index 00000000..663b42b5
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java
@@ -0,0 +1,4 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+public class NoSessionsCreatedException extends Throwable{
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java
new file mode 100644
index 00000000..3d40a408
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+public interface OnMessageCreatedCallback {
+ void run(XmppAxolotlMessage message);
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
new file mode 100644
index 00000000..190eb88a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java
@@ -0,0 +1,421 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.util.Log;
+import android.util.LruCache;
+
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.IdentityKeyPair;
+import org.whispersystems.libaxolotl.InvalidKeyIdException;
+import org.whispersystems.libaxolotl.ecc.Curve;
+import org.whispersystems.libaxolotl.ecc.ECKeyPair;
+import org.whispersystems.libaxolotl.state.AxolotlStore;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SessionRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+import org.whispersystems.libaxolotl.util.KeyHelper;
+
+import java.util.List;
+import java.util.Set;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+public class SQLiteAxolotlStore implements AxolotlStore {
+
+ public static final String PREKEY_TABLENAME = "prekeys";
+ public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
+ public static final String SESSION_TABLENAME = "sessions";
+ public static final String IDENTITIES_TABLENAME = "identities";
+ public static final String ACCOUNT = "account";
+ public static final String DEVICE_ID = "device_id";
+ public static final String ID = "id";
+ public static final String KEY = "key";
+ public static final String FINGERPRINT = "fingerprint";
+ public static final String NAME = "name";
+ public static final String TRUSTED = "trusted";
+ public static final String OWN = "ownkey";
+
+ public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
+ public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id";
+
+ private static final int NUM_TRUSTS_TO_CACHE = 100;
+
+ private final Account account;
+ private final XmppConnectionService mXmppConnectionService;
+
+ private IdentityKeyPair identityKeyPair;
+ private int localRegistrationId;
+ private int currentPreKeyId = 0;
+
+ private final LruCache<String, XmppAxolotlSession.Trust> trustCache =
+ new LruCache<String, XmppAxolotlSession.Trust>(NUM_TRUSTS_TO_CACHE) {
+ @Override
+ protected XmppAxolotlSession.Trust create(String fingerprint) {
+ return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
+ }
+ };
+
+ private static IdentityKeyPair generateIdentityKeyPair() {
+ Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair...");
+ ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
+ return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
+ identityKeyPairKeys.getPrivateKey());
+ }
+
+ private static int generateRegistrationId() {
+ Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID...");
+ return KeyHelper.generateRegistrationId(true);
+ }
+
+ public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
+ this.account = account;
+ this.mXmppConnectionService = service;
+ this.localRegistrationId = loadRegistrationId();
+ this.currentPreKeyId = loadCurrentPreKeyId();
+ for (SignedPreKeyRecord record : loadSignedPreKeys()) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId());
+ }
+ }
+
+ public int getCurrentPreKeyId() {
+ return currentPreKeyId;
+ }
+
+ // --------------------------------------
+ // IdentityKeyStore
+ // --------------------------------------
+
+ private IdentityKeyPair loadIdentityKeyPair() {
+ String ownName = account.getJid().toBareJid().toString();
+ IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account,
+ ownName);
+
+ if (ownKey != null) {
+ return ownKey;
+ } else {
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl key for account " + ownName);
+ ownKey = generateIdentityKeyPair();
+ mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey);
+ }
+ return ownKey;
+ }
+
+ private int loadRegistrationId() {
+ return loadRegistrationId(false);
+ }
+
+ private int loadRegistrationId(boolean regenerate) {
+ String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
+ int reg_id;
+ if (!regenerate && regIdString != null) {
+ reg_id = Integer.valueOf(regIdString);
+ } else {
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid());
+ reg_id = generateRegistrationId();
+ boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id));
+ if (success) {
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } else {
+ Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!");
+ }
+ }
+ return reg_id;
+ }
+
+ private int loadCurrentPreKeyId() {
+ String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
+ int reg_id;
+ if (regIdString != null) {
+ reg_id = Integer.valueOf(regIdString);
+ } else {
+ Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid());
+ reg_id = 0;
+ }
+ return reg_id;
+ }
+
+ public void regenerate() {
+ mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
+ trustCache.evictAll();
+ account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
+ identityKeyPair = loadIdentityKeyPair();
+ localRegistrationId = loadRegistrationId(true);
+ currentPreKeyId = 0;
+ mXmppConnectionService.updateAccountUi();
+ }
+
+ /**
+ * Get the local client's identity key pair.
+ *
+ * @return The local client's persistent identity key pair.
+ */
+ @Override
+ public IdentityKeyPair getIdentityKeyPair() {
+ if (identityKeyPair == null) {
+ identityKeyPair = loadIdentityKeyPair();
+ }
+ return identityKeyPair;
+ }
+
+ /**
+ * Return the local client's registration ID.
+ * <p/>
+ * Clients should maintain a registration ID, a random number
+ * between 1 and 16380 that's generated once at install time.
+ *
+ * @return the local client's registration ID.
+ */
+ @Override
+ public int getLocalRegistrationId() {
+ return localRegistrationId;
+ }
+
+ /**
+ * Save a remote client's identity key
+ * <p/>
+ * Store a remote client's identity key as trusted.
+ *
+ * @param name The name of the remote client.
+ * @param identityKey The remote client's identity key.
+ */
+ @Override
+ public void saveIdentity(String name, IdentityKey identityKey) {
+ if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
+ mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
+ }
+ }
+
+ /**
+ * Verify a remote client's identity key.
+ * <p/>
+ * Determine whether a remote client's identity is trusted. Convention is
+ * that the TextSecure protocol is 'trust on first use.' This means that
+ * an identity key is considered 'trusted' if there is no entry for the recipient
+ * in the local store, or if it matches the saved key for a recipient in the local
+ * store. Only if it mismatches an entry in the local store is it considered
+ * 'untrusted.'
+ *
+ * @param name The name of the remote client.
+ * @param identityKey The identity key to verify.
+ * @return true if trusted, false if untrusted.
+ */
+ @Override
+ public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
+ return true;
+ }
+
+ public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
+ return (fingerprint == null)? null : trustCache.get(fingerprint);
+ }
+
+ public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
+ mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
+ trustCache.remove(fingerprint);
+ }
+
+ public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) {
+ return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust);
+ }
+
+ public long getContactNumTrustedKeys(String bareJid) {
+ return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid);
+ }
+
+ // --------------------------------------
+ // SessionStore
+ // --------------------------------------
+
+ /**
+ * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
+ * or a new SessionRecord if one does not currently exist.
+ * <p/>
+ * It is important that implementations return a copy of the current durable information. The
+ * returned SessionRecord may be modified, but those changes should not have an effect on the
+ * durable session state (what is returned by subsequent calls to this method) without the
+ * store method being called here first.
+ *
+ * @param address The name and device ID of the remote client.
+ * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
+ * a new SessionRecord if one does not currently exist.
+ */
+ @Override
+ public SessionRecord loadSession(AxolotlAddress address) {
+ SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
+ return (session != null) ? session : new SessionRecord();
+ }
+
+ /**
+ * Returns all known devices with active sessions for a recipient
+ *
+ * @param name the name of the client.
+ * @return all known sub-devices with active sessions.
+ */
+ @Override
+ public List<Integer> getSubDeviceSessions(String name) {
+ return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
+ new AxolotlAddress(name, 0));
+ }
+
+ /**
+ * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ * @param record the current SessionRecord for the remote client.
+ */
+ @Override
+ public void storeSession(AxolotlAddress address, SessionRecord record) {
+ mXmppConnectionService.databaseBackend.storeSession(account, address, record);
+ }
+
+ /**
+ * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ * @return true if a {@link SessionRecord} exists, false otherwise.
+ */
+ @Override
+ public boolean containsSession(AxolotlAddress address) {
+ return mXmppConnectionService.databaseBackend.containsSession(account, address);
+ }
+
+ /**
+ * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ */
+ @Override
+ public void deleteSession(AxolotlAddress address) {
+ mXmppConnectionService.databaseBackend.deleteSession(account, address);
+ }
+
+ /**
+ * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
+ *
+ * @param name the name of the remote client.
+ */
+ @Override
+ public void deleteAllSessions(String name) {
+ AxolotlAddress address = new AxolotlAddress(name, 0);
+ mXmppConnectionService.databaseBackend.deleteAllSessions(account,
+ address);
+ }
+
+ // --------------------------------------
+ // PreKeyStore
+ // --------------------------------------
+
+ /**
+ * Load a local PreKeyRecord.
+ *
+ * @param preKeyId the ID of the local PreKeyRecord.
+ * @return the corresponding PreKeyRecord.
+ * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
+ */
+ @Override
+ public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
+ PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
+ if (record == null) {
+ throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
+ }
+ return record;
+ }
+
+ /**
+ * Store a local PreKeyRecord.
+ *
+ * @param preKeyId the ID of the PreKeyRecord to store.
+ * @param record the PreKeyRecord.
+ */
+ @Override
+ public void storePreKey(int preKeyId, PreKeyRecord record) {
+ mXmppConnectionService.databaseBackend.storePreKey(account, record);
+ currentPreKeyId = preKeyId;
+ boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId));
+ if (success) {
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } else {
+ Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!");
+ }
+ }
+
+ /**
+ * @param preKeyId A PreKeyRecord ID.
+ * @return true if the store has a record for the preKeyId, otherwise false.
+ */
+ @Override
+ public boolean containsPreKey(int preKeyId) {
+ return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
+ }
+
+ /**
+ * Delete a PreKeyRecord from local storage.
+ *
+ * @param preKeyId The ID of the PreKeyRecord to remove.
+ */
+ @Override
+ public void removePreKey(int preKeyId) {
+ mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
+ }
+
+ // --------------------------------------
+ // SignedPreKeyStore
+ // --------------------------------------
+
+ /**
+ * Load a local SignedPreKeyRecord.
+ *
+ * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
+ * @return the corresponding SignedPreKeyRecord.
+ * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
+ */
+ @Override
+ public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
+ SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
+ if (record == null) {
+ throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
+ }
+ return record;
+ }
+
+ /**
+ * Load all local SignedPreKeyRecords.
+ *
+ * @return All stored SignedPreKeyRecords.
+ */
+ @Override
+ public List<SignedPreKeyRecord> loadSignedPreKeys() {
+ return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
+ }
+
+ /**
+ * Store a local SignedPreKeyRecord.
+ *
+ * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
+ * @param record the SignedPreKeyRecord.
+ */
+ @Override
+ public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
+ mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
+ }
+
+ /**
+ * @param signedPreKeyId A SignedPreKeyRecord ID.
+ * @return true if the store has a record for the signedPreKeyId, otherwise false.
+ */
+ @Override
+ public boolean containsSignedPreKey(int signedPreKeyId) {
+ return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
+ }
+
+ /**
+ * Delete a SignedPreKeyRecord from local storage.
+ *
+ * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
+ */
+ @Override
+ public void removeSignedPreKey(int signedPreKeyId) {
+ mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java
new file mode 100644
index 00000000..cf950d6d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java
@@ -0,0 +1,249 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.util.Base64;
+import android.util.Log;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class XmppAxolotlMessage {
+ public static final String CONTAINERTAG = "encrypted";
+ public static final String HEADER = "header";
+ public static final String SOURCEID = "sid";
+ public static final String KEYTAG = "key";
+ public static final String REMOTEID = "rid";
+ public static final String IVTAG = "iv";
+ public static final String PAYLOAD = "payload";
+
+ private static final String KEYTYPE = "AES";
+ private static final String CIPHERMODE = "AES/GCM/NoPadding";
+ private static final String PROVIDER = "BC";
+
+ private byte[] innerKey;
+ private byte[] ciphertext = null;
+ private byte[] iv = null;
+ private final Map<Integer, byte[]> keys;
+ private final Jid from;
+ private final int sourceDeviceId;
+
+ public static class XmppAxolotlPlaintextMessage {
+ private final String plaintext;
+ private final String fingerprint;
+
+ public XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) {
+ this.plaintext = plaintext;
+ this.fingerprint = fingerprint;
+ }
+
+ public String getPlaintext() {
+ return plaintext;
+ }
+
+
+ public String getFingerprint() {
+ return fingerprint;
+ }
+ }
+
+ public static class XmppAxolotlKeyTransportMessage {
+ private final String fingerprint;
+ private final byte[] key;
+ private final byte[] iv;
+
+ public XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) {
+ this.fingerprint = fingerprint;
+ this.key = key;
+ this.iv = iv;
+ }
+
+ public String getFingerprint() {
+ return fingerprint;
+ }
+
+ public byte[] getKey() {
+ return key;
+ }
+
+ public byte[] getIv() {
+ return iv;
+ }
+ }
+
+ private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
+ this.from = from;
+ Element header = axolotlMessage.findChild(HEADER);
+ this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
+ List<Element> keyElements = header.getChildren();
+ this.keys = new HashMap<>(keyElements.size());
+ for (Element keyElement : keyElements) {
+ switch (keyElement.getName()) {
+ case KEYTAG:
+ try {
+ Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
+ byte[] key = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
+ this.keys.put(recipientId, key);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(e);
+ }
+ break;
+ case IVTAG:
+ if (this.iv != null) {
+ throw new IllegalArgumentException("Duplicate iv entry");
+ }
+ iv = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
+ break;
+ default:
+ Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString());
+ break;
+ }
+ }
+ Element payloadElement = axolotlMessage.findChild(PAYLOAD);
+ if (payloadElement != null) {
+ ciphertext = Base64.decode(payloadElement.getContent(), Base64.DEFAULT);
+ }
+ }
+
+ public XmppAxolotlMessage(Jid from, int sourceDeviceId) {
+ this.from = from;
+ this.sourceDeviceId = sourceDeviceId;
+ this.keys = new HashMap<>();
+ this.iv = generateIv();
+ this.innerKey = generateKey();
+ }
+
+ public static XmppAxolotlMessage fromElement(Element element, Jid from) {
+ return new XmppAxolotlMessage(element, from);
+ }
+
+ private static byte[] generateKey() {
+ try {
+ KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE);
+ generator.init(128);
+ return generator.generateKey().getEncoded();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(Config.LOGTAG, e.getMessage());
+ return null;
+ }
+ }
+
+ private static byte[] generateIv() {
+ SecureRandom random = new SecureRandom();
+ byte[] iv = new byte[16];
+ random.nextBytes(iv);
+ return iv;
+ }
+
+ public void encrypt(String plaintext) throws CryptoFailedException {
+ try {
+ SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
+ this.innerKey = secretKey.getEncoded();
+ this.ciphertext = cipher.doFinal(plaintext.getBytes());
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
+ | InvalidAlgorithmParameterException e) {
+ throw new CryptoFailedException(e);
+ }
+ }
+
+ public Jid getFrom() {
+ return this.from;
+ }
+
+ public int getSenderDeviceId() {
+ return sourceDeviceId;
+ }
+
+ public byte[] getCiphertext() {
+ return ciphertext;
+ }
+
+ public void addDevice(XmppAxolotlSession session) {
+ byte[] key = session.processSending(innerKey);
+ if (key != null) {
+ keys.put(session.getRemoteAddress().getDeviceId(), key);
+ }
+ }
+
+ public byte[] getInnerKey() {
+ return innerKey;
+ }
+
+ public byte[] getIV() {
+ return this.iv;
+ }
+
+ public Element toElement() {
+ Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX);
+ Element headerElement = encryptionElement.addChild(HEADER);
+ headerElement.setAttribute(SOURCEID, sourceDeviceId);
+ for (Map.Entry<Integer, byte[]> keyEntry : keys.entrySet()) {
+ Element keyElement = new Element(KEYTAG);
+ keyElement.setAttribute(REMOTEID, keyEntry.getKey());
+ keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.DEFAULT));
+ headerElement.addChild(keyElement);
+ }
+ headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT));
+ if (ciphertext != null) {
+ Element payload = encryptionElement.addChild(PAYLOAD);
+ payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT));
+ }
+ return encryptionElement;
+ }
+
+ private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) {
+ byte[] encryptedKey = keys.get(sourceDeviceId);
+ return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null;
+ }
+
+ public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) {
+ byte[] key = unpackKey(session, sourceDeviceId);
+ return (key != null)
+ ? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV())
+ : null;
+ }
+
+ public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
+ XmppAxolotlPlaintextMessage plaintextMessage = null;
+ byte[] key = unpackKey(session, sourceDeviceId);
+ if (key != null) {
+ try {
+ Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
+ SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+
+ String plaintext = new String(cipher.doFinal(ciphertext));
+ plaintextMessage = new XmppAxolotlPlaintextMessage(plaintext, session.getFingerprint());
+
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | InvalidAlgorithmParameterException | IllegalBlockSizeException
+ | BadPaddingException | NoSuchProviderException e) {
+ throw new CryptoFailedException(e);
+ }
+ }
+ return plaintextMessage;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java
new file mode 100644
index 00000000..c4053854
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java
@@ -0,0 +1,196 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.DuplicateMessageException;
+import org.whispersystems.libaxolotl.InvalidKeyException;
+import org.whispersystems.libaxolotl.InvalidKeyIdException;
+import org.whispersystems.libaxolotl.InvalidMessageException;
+import org.whispersystems.libaxolotl.InvalidVersionException;
+import org.whispersystems.libaxolotl.LegacyMessageException;
+import org.whispersystems.libaxolotl.NoSessionException;
+import org.whispersystems.libaxolotl.SessionCipher;
+import org.whispersystems.libaxolotl.UntrustedIdentityException;
+import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
+import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
+import org.whispersystems.libaxolotl.protocol.WhisperMessage;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+
+public class XmppAxolotlSession {
+ private final SessionCipher cipher;
+ private final SQLiteAxolotlStore sqLiteAxolotlStore;
+ private final AxolotlAddress remoteAddress;
+ private final Account account;
+ private String fingerprint = null;
+ private Integer preKeyId = null;
+ private boolean fresh = true;
+
+ public enum Trust {
+ UNDECIDED(0),
+ TRUSTED(1),
+ UNTRUSTED(2),
+ COMPROMISED(3),
+ INACTIVE_TRUSTED(4),
+ INACTIVE_UNDECIDED(5),
+ INACTIVE_UNTRUSTED(6);
+
+ private static final Map<Integer, Trust> trustsByValue = new HashMap<>();
+
+ static {
+ for (Trust trust : Trust.values()) {
+ trustsByValue.put(trust.getCode(), trust);
+ }
+ }
+
+ private final int code;
+
+ Trust(int code) {
+ this.code = code;
+ }
+
+ public int getCode() {
+ return this.code;
+ }
+
+ public String toString() {
+ switch (this) {
+ case UNDECIDED:
+ return "Trust undecided " + getCode();
+ case TRUSTED:
+ return "Trusted " + getCode();
+ case COMPROMISED:
+ return "Compromised " + getCode();
+ case INACTIVE_TRUSTED:
+ return "Inactive (Trusted)" + getCode();
+ case INACTIVE_UNDECIDED:
+ return "Inactive (Undecided)" + getCode();
+ case INACTIVE_UNTRUSTED:
+ return "Inactive (Untrusted)" + getCode();
+ case UNTRUSTED:
+ default:
+ return "Untrusted " + getCode();
+ }
+ }
+
+ public static Trust fromBoolean(Boolean trusted) {
+ return trusted ? TRUSTED : UNTRUSTED;
+ }
+
+ public static Trust fromCode(int code) {
+ return trustsByValue.get(code);
+ }
+ }
+
+ public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) {
+ this(account, store, remoteAddress);
+ this.fingerprint = fingerprint;
+ }
+
+ public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) {
+ this.cipher = new SessionCipher(store, remoteAddress);
+ this.remoteAddress = remoteAddress;
+ this.sqLiteAxolotlStore = store;
+ this.account = account;
+ }
+
+ public Integer getPreKeyId() {
+ return preKeyId;
+ }
+
+ public void resetPreKeyId() {
+
+ preKeyId = null;
+ }
+
+ public String getFingerprint() {
+ return fingerprint;
+ }
+
+ public AxolotlAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ public boolean isFresh() {
+ return fresh;
+ }
+
+ public void setNotFresh() {
+ this.fresh = false;
+ }
+
+ protected void setTrust(Trust trust) {
+ sqLiteAxolotlStore.setFingerprintTrust(fingerprint, trust);
+ }
+
+ protected Trust getTrust() {
+ Trust trust = sqLiteAxolotlStore.getFingerprintTrust(fingerprint);
+ return (trust == null) ? Trust.UNDECIDED : trust;
+ }
+
+ @Nullable
+ public byte[] processReceiving(byte[] encryptedKey) {
+ byte[] plaintext = null;
+ Trust trust = getTrust();
+ switch (trust) {
+ case INACTIVE_TRUSTED:
+ case UNDECIDED:
+ case UNTRUSTED:
+ case TRUSTED:
+ try {
+ try {
+ PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey);
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
+ String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", "");
+ if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) {
+ Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.fingerprint + ", received message with fingerprint " + fingerprint);
+ } else {
+ this.fingerprint = fingerprint;
+ plaintext = cipher.decrypt(message);
+ if (message.getPreKeyId().isPresent()) {
+ preKeyId = message.getPreKeyId().get();
+ }
+ }
+ } catch (InvalidMessageException | InvalidVersionException e) {
+ Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received");
+ WhisperMessage message = new WhisperMessage(encryptedKey);
+ plaintext = cipher.decrypt(message);
+ } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
+ Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
+ }
+ } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
+ Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
+ }
+
+ if (plaintext != null && trust == Trust.INACTIVE_TRUSTED) {
+ setTrust(Trust.TRUSTED);
+ }
+
+ break;
+
+ case COMPROMISED:
+ default:
+ // ignore
+ break;
+ }
+ return plaintext;
+ }
+
+ @Nullable
+ public byte[] processSending(@NonNull byte[] outgoingMessage) {
+ Trust trust = getTrust();
+ if (trust == Trust.TRUSTED) {
+ CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
+ return ciphertextMessage.serialize();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
index c95a62df..f47677f6 100644
--- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
+++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java
@@ -101,6 +101,9 @@ public class ScramSha1 extends SaslMechanism {
public String getResponse(final String challenge) throws AuthenticationException {
switch (state) {
case AUTH_TEXT_SENT:
+ if (challenge == null) {
+ throw new AuthenticationException("challenge can not be null");
+ }
serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
String nonce = "";
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
index 90c10199..a8b9252f 100644
--- a/src/main/java/eu/siacs/conversations/entities/Account.java
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -20,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrService;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
@@ -122,6 +123,7 @@ public class Account extends AbstractEntity {
protected String avatar;
protected boolean online = false;
private OtrService mOtrService = null;
+ private AxolotlService axolotlService = null;
private XmppConnection xmppConnection = null;
private long mEndGracePeriod = 0L;
private String otrFingerprint;
@@ -255,6 +257,10 @@ public class Account extends AbstractEntity {
return keys;
}
+ public String getKey(final String name) {
+ return this.keys.optString(name, null);
+ }
+
public boolean setKey(final String keyName, final String keyValue) {
try {
this.keys.put(keyName, keyValue);
@@ -278,9 +284,13 @@ public class Account extends AbstractEntity {
return values;
}
+ public AxolotlService getAxolotlService() {
+ return axolotlService;
+ }
+
public void initAccountServices(final XmppConnectionService context) {
- this.mXmppConnectionService = context;
this.mOtrService = new OtrService(context, this);
+ this.axolotlService = new AxolotlService(this, context);
}
public OtrService getOtrService() {
diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java
index 81f6568b..2d103faf 100644
--- a/src/main/java/eu/siacs/conversations/entities/Contact.java
+++ b/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -189,20 +189,22 @@ public class Contact implements ListItem, Blockable {
}
public ContentValues getContentValues() {
- final ContentValues values = new ContentValues();
- values.put(ACCOUNT, accountUuid);
- values.put(SYSTEMNAME, systemName);
- values.put(SERVERNAME, serverName);
- values.put(JID, jid.toString());
- values.put(OPTIONS, subscription);
- values.put(SYSTEMACCOUNT, systemAccount);
- values.put(PHOTOURI, photoUri);
- values.put(KEYS, keys.toString());
- values.put(AVATAR, avatar == null ? null : avatar.getFilename());
- values.put(LAST_PRESENCE, lastseen.presence);
- values.put(LAST_TIME, lastseen.time);
- values.put(GROUPS, groups.toString());
- return values;
+ synchronized (this.keys) {
+ final ContentValues values = new ContentValues();
+ values.put(ACCOUNT, accountUuid);
+ values.put(SYSTEMNAME, systemName);
+ values.put(SERVERNAME, serverName);
+ values.put(JID, jid.toString());
+ values.put(OPTIONS, subscription);
+ values.put(SYSTEMACCOUNT, systemAccount);
+ values.put(PHOTOURI, photoUri);
+ values.put(KEYS, keys.toString());
+ values.put(AVATAR, avatar == null ? null : avatar.getFilename());
+ values.put(LAST_PRESENCE, lastseen.presence);
+ values.put(LAST_TIME, lastseen.time);
+ values.put(GROUPS, groups.toString());
+ return values;
+ }
}
public int getSubscription() {
@@ -287,60 +289,65 @@ public class Contact implements ListItem, Blockable {
}
public ArrayList<String> getOtrFingerprints() {
- final ArrayList<String> fingerprints = new ArrayList<String>();
- try {
- if (this.keys.has("otr_fingerprints")) {
- final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
- for (int i = 0; i < prints.length(); ++i) {
- final String print = prints.isNull(i) ? null : prints.getString(i);
- if (print != null && !print.isEmpty()) {
- fingerprints.add(prints.getString(i));
+ synchronized (this.keys) {
+ final ArrayList<String> fingerprints = new ArrayList<String>();
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
+ for (int i = 0; i < prints.length(); ++i) {
+ final String print = prints.isNull(i) ? null : prints.getString(i);
+ if (print != null && !print.isEmpty()) {
+ fingerprints.add(prints.getString(i));
+ }
}
}
- }
- } catch (final JSONException ignored) {
+ } catch (final JSONException ignored) {
+ }
+ return fingerprints;
}
- return fingerprints;
}
-
public boolean addOtrFingerprint(String print) {
- if (getOtrFingerprints().contains(print)) {
- return false;
- }
- try {
- JSONArray fingerprints;
- if (!this.keys.has("otr_fingerprints")) {
- fingerprints = new JSONArray();
-
- } else {
- fingerprints = this.keys.getJSONArray("otr_fingerprints");
+ synchronized (this.keys) {
+ if (getOtrFingerprints().contains(print)) {
+ return false;
+ }
+ try {
+ JSONArray fingerprints;
+ if (!this.keys.has("otr_fingerprints")) {
+ fingerprints = new JSONArray();
+ } else {
+ fingerprints = this.keys.getJSONArray("otr_fingerprints");
+ }
+ fingerprints.put(print);
+ this.keys.put("otr_fingerprints", fingerprints);
+ return true;
+ } catch (final JSONException ignored) {
+ return false;
}
- fingerprints.put(print);
- this.keys.put("otr_fingerprints", fingerprints);
- return true;
- } catch (final JSONException ignored) {
- return false;
}
}
public long getPgpKeyId() {
- if (this.keys.has("pgp_keyid")) {
- try {
- return this.keys.getLong("pgp_keyid");
- } catch (JSONException e) {
+ synchronized (this.keys) {
+ if (this.keys.has("pgp_keyid")) {
+ try {
+ return this.keys.getLong("pgp_keyid");
+ } catch (JSONException e) {
+ return 0;
+ }
+ } else {
return 0;
}
- } else {
- return 0;
}
}
public void setPgpKeyId(long keyId) {
- try {
- this.keys.put("pgp_keyid", keyId);
- } catch (final JSONException ignored) {
-
+ synchronized (this.keys) {
+ try {
+ this.keys.put("pgp_keyid", keyId);
+ } catch (final JSONException ignored) {
+ }
}
}
@@ -447,24 +454,26 @@ public class Contact implements ListItem, Blockable {
}
public boolean deleteOtrFingerprint(String fingerprint) {
- boolean success = false;
- try {
- if (this.keys.has("otr_fingerprints")) {
- JSONArray newPrints = new JSONArray();
- JSONArray oldPrints = this.keys
- .getJSONArray("otr_fingerprints");
- for (int i = 0; i < oldPrints.length(); ++i) {
- if (!oldPrints.getString(i).equals(fingerprint)) {
- newPrints.put(oldPrints.getString(i));
- } else {
- success = true;
+ synchronized (this.keys) {
+ boolean success = false;
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ JSONArray newPrints = new JSONArray();
+ JSONArray oldPrints = this.keys
+ .getJSONArray("otr_fingerprints");
+ for (int i = 0; i < oldPrints.length(); ++i) {
+ if (!oldPrints.getString(i).equals(fingerprint)) {
+ newPrints.put(oldPrints.getString(i));
+ } else {
+ success = true;
+ }
}
+ this.keys.put("otr_fingerprints", newPrints);
}
- this.keys.put("otr_fingerprints", newPrints);
+ return success;
+ } catch (JSONException e) {
+ return false;
}
- return success;
- } catch (JSONException e) {
- return false;
}
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index 6ff0e270..1c985e2f 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -179,13 +179,13 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
- public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) {
+ public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
synchronized (this.messages) {
for (Message message : this.messages) {
if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
- && (message.getEncryption() == Message.ENCRYPTION_OTR)) {
+ && (message.getEncryption() == encryptionType)) {
onMessageFound.onMessageFound(message);
- }
+ }
}
}
}
@@ -519,6 +519,13 @@ public class Conversation extends AbstractEntity implements Blockable {
return getContact().getOtrFingerprints().contains(getOtrFingerprint());
}
+ /**
+ * short for is Private and Non-anonymous
+ */
+ public boolean isPnNA() {
+ return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
+ }
+
public synchronized MucOptions getMucOptions() {
if (this.mucOptions == null) {
this.mucOptions = new MucOptions(this);
@@ -542,42 +549,51 @@ public class Conversation extends AbstractEntity implements Blockable {
return this.nextCounterpart;
}
- public int getLatestEncryption() {
- int latestEncryption = this.getLatestMessage().getEncryption();
- if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
- || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
- return Message.ENCRYPTION_PGP;
- } else {
- return latestEncryption;
+ private int getMostRecentlyUsedOutgoingEncryption() {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() -1; i >= 0; --i) {
+ final Message m = this.messages.get(i);
+ if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
+ final int e = m.getEncryption();
+ if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
+ return Message.ENCRYPTION_PGP;
+ } else {
+ return e;
+ }
+ }
+ }
}
+ return Message.ENCRYPTION_NONE;
}
- public int getNextEncryption(boolean force) {
- int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
- if (next == -1) {
- int latest = this.getLatestEncryption();
- if (latest == Message.ENCRYPTION_NONE) {
- if (force && getMode() == MODE_SINGLE) {
- return Message.ENCRYPTION_OTR;
- } else if (getContact().getPresences().size() == 1) {
- if (getContact().getOtrFingerprints().size() >= 1) {
- return Message.ENCRYPTION_OTR;
+ private int getMostRecentlyUsedIncomingEncryption() {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() -1; i >= 0; --i) {
+ final Message m = this.messages.get(i);
+ if (m.getStatus() == Message.STATUS_RECEIVED) {
+ final int e = m.getEncryption();
+ if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
+ return Message.ENCRYPTION_PGP;
} else {
- return latest;
+ return e;
}
- } else {
- return latest;
}
- } else {
- return latest;
}
}
- if (next == Message.ENCRYPTION_NONE && force
- && getMode() == MODE_SINGLE) {
- return Message.ENCRYPTION_OTR;
- } else {
- return next;
+ return Message.ENCRYPTION_NONE;
+ }
+
+ public int getNextEncryption() {
+ int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
+ if (next == -1) {
+ int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
+ if (outgoing == Message.ENCRYPTION_NONE) {
+ return this.getMostRecentlyUsedIncomingEncryption();
+ } else {
+ return outgoing;
+ }
}
+ return next;
}
public void setNextEncryption(int encryption) {
diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
index ae9ba1f1..d35a4b01 100644
--- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
+++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java
@@ -1,28 +1,8 @@
package eu.siacs.conversations.entities;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URLConnection;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.Cipher;
-import javax.crypto.CipherInputStream;
-import javax.crypto.CipherOutputStream;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.utils.MimeUtils;
-import android.util.Log;
+import eu.siacs.conversations.utils.MimeUtils;
public class DownloadableFile extends File {
@@ -30,8 +10,7 @@ public class DownloadableFile extends File {
private long expectedSize = 0;
private String sha1sum;
- private Key aeskey;
- private String mime;
+ private byte[] aeskey;
private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
@@ -45,15 +24,7 @@ public class DownloadableFile extends File {
}
public long getExpectedSize() {
- if (this.aeskey != null) {
- if (this.expectedSize == 0) {
- return 0;
- } else {
- return (this.expectedSize / 16 + 1) * 16;
- }
- } else {
- return this.expectedSize;
- }
+ return this.expectedSize;
}
public String getMimeType() {
@@ -79,91 +50,38 @@ public class DownloadableFile extends File {
this.sha1sum = sum;
}
- public void setKey(byte[] key) {
- if (key.length == 48) {
+ public void setKeyAndIv(byte[] keyIvCombo) {
+ if (keyIvCombo.length == 48) {
byte[] secretKey = new byte[32];
byte[] iv = new byte[16];
- System.arraycopy(key, 0, iv, 0, 16);
- System.arraycopy(key, 16, secretKey, 0, 32);
- this.aeskey = new SecretKeySpec(secretKey, "AES");
+ System.arraycopy(keyIvCombo, 0, iv, 0, 16);
+ System.arraycopy(keyIvCombo, 16, secretKey, 0, 32);
+ this.aeskey = secretKey;
this.iv = iv;
- } else if (key.length >= 32) {
+ } else if (keyIvCombo.length >= 32) {
byte[] secretKey = new byte[32];
- System.arraycopy(key, 0, secretKey, 0, 32);
- this.aeskey = new SecretKeySpec(secretKey, "AES");
- } else if (key.length >= 16) {
+ System.arraycopy(keyIvCombo, 0, secretKey, 0, 32);
+ this.aeskey = secretKey;
+ } else if (keyIvCombo.length >= 16) {
byte[] secretKey = new byte[16];
- System.arraycopy(key, 0, secretKey, 0, 16);
- this.aeskey = new SecretKeySpec(secretKey, "AES");
+ System.arraycopy(keyIvCombo, 0, secretKey, 0, 16);
+ this.aeskey = secretKey;
}
}
- public Key getKey() {
- return this.aeskey;
+ public void setKey(byte[] key) {
+ this.aeskey = key;
}
- public InputStream createInputStream() {
- if (this.getKey() == null) {
- try {
- return new FileInputStream(this);
- } catch (FileNotFoundException e) {
- return null;
- }
- } else {
- try {
- IvParameterSpec ips = new IvParameterSpec(iv);
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
- cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips);
- Log.d(Config.LOGTAG, "opening encrypted input stream");
- return new CipherInputStream(new FileInputStream(this), cipher);
- } catch (NoSuchAlgorithmException e) {
- Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
- return null;
- } catch (NoSuchPaddingException e) {
- Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
- return null;
- } catch (InvalidKeyException e) {
- Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
- return null;
- } catch (InvalidAlgorithmParameterException e) {
- Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
- return null;
- } catch (FileNotFoundException e) {
- return null;
- }
- }
+ public void setIv(byte[] iv) {
+ this.iv = iv;
}
- public OutputStream createOutputStream() {
- if (this.getKey() == null) {
- try {
- return new FileOutputStream(this);
- } catch (FileNotFoundException e) {
- return null;
- }
- } else {
- try {
- IvParameterSpec ips = new IvParameterSpec(this.iv);
- Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
- cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips);
- Log.d(Config.LOGTAG, "opening encrypted output stream");
- return new CipherOutputStream(new FileOutputStream(this),
- cipher);
- } catch (NoSuchAlgorithmException e) {
- Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
- return null;
- } catch (NoSuchPaddingException e) {
- Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
- return null;
- } catch (InvalidKeyException e) {
- Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
- return null;
- } catch (InvalidAlgorithmParameterException e) {
- Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
- return null;
- } catch (FileNotFoundException e) {
- return null;
- }
- }
+ public byte[] getKey() {
+ return this.aeskey;
+ }
+
+ public byte[] getIv() {
+ return this.iv;
}
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index d04dff87..6a878e90 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -8,6 +8,7 @@ import java.net.URL;
import java.util.Arrays;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper;
@@ -34,6 +35,7 @@ public class Message extends AbstractEntity {
public static final int ENCRYPTION_OTR = 2;
public static final int ENCRYPTION_DECRYPTED = 3;
public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
+ public static final int ENCRYPTION_AXOLOTL = 5;
public static final int TYPE_TEXT = 0;
public static final int TYPE_IMAGE = 1;
@@ -49,9 +51,11 @@ public class Message extends AbstractEntity {
public static final String ENCRYPTION = "encryption";
public static final String STATUS = "status";
public static final String TYPE = "type";
+ public static final String CARBON = "carbon";
public static final String REMOTE_MSG_ID = "remoteMsgId";
public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
+ public static final String FINGERPRINT = "axolotl_fingerprint";
public static final String ME_COMMAND = "/me ";
@@ -65,6 +69,7 @@ public class Message extends AbstractEntity {
protected int encryption;
protected int status;
protected int type;
+ protected boolean carbon = false;
protected String relativeFilePath;
protected boolean read = true;
protected String remoteMsgId = null;
@@ -73,6 +78,7 @@ public class Message extends AbstractEntity {
protected Transferable transferable = null;
private Message mNextMessage = null;
private Message mPreviousMessage = null;
+ private String axolotlFingerprint = null;
private Message() {
@@ -81,8 +87,11 @@ public class Message extends AbstractEntity {
public Message(Conversation conversation, String body, int encryption) {
this(conversation, body, encryption, STATUS_UNSEND);
}
-
public Message(Conversation conversation, String body, int encryption, int status) {
+ this(conversation, body, encryption, status, false);
+ }
+
+ public Message(Conversation conversation, String body, int encryption, int status, boolean carbon) {
this(java.util.UUID.randomUUID().toString(),
conversation.getUuid(),
conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
@@ -92,6 +101,8 @@ public class Message extends AbstractEntity {
encryption,
status,
TYPE_TEXT,
+ false,
+ null,
null,
null,
null);
@@ -100,8 +111,9 @@ public class Message extends AbstractEntity {
private Message(final String uuid, final String conversationUUid, final Jid counterpart,
final Jid trueCounterpart, final String body, final long timeSent,
- final int encryption, final int status, final int type, final String remoteMsgId,
- final String relativeFilePath, final String serverMsgId) {
+ final int encryption, final int status, final int type, final boolean carbon,
+ final String remoteMsgId, final String relativeFilePath,
+ final String serverMsgId, final String fingerprint) {
this.uuid = uuid;
this.conversationUuid = conversationUUid;
this.counterpart = counterpart;
@@ -111,9 +123,11 @@ public class Message extends AbstractEntity {
this.encryption = encryption;
this.status = status;
this.type = type;
+ this.carbon = carbon;
this.remoteMsgId = remoteMsgId;
this.relativeFilePath = relativeFilePath;
this.serverMsgId = serverMsgId;
+ this.axolotlFingerprint = fingerprint;
}
public static Message fromCursor(Cursor cursor) {
@@ -148,9 +162,11 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(TYPE)),
+ cursor.getInt(cursor.getColumnIndex(CARBON))>0,
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
- cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
+ cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
+ cursor.getString(cursor.getColumnIndex(FINGERPRINT)));
}
public static Message createStatusMessage(Conversation conversation, String body) {
@@ -181,9 +197,11 @@ public class Message extends AbstractEntity {
values.put(ENCRYPTION, encryption);
values.put(STATUS, status);
values.put(TYPE, type);
+ values.put(CARBON, carbon ? 1 : 0);
values.put(REMOTE_MSG_ID, remoteMsgId);
values.put(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId);
+ values.put(FINGERPRINT, axolotlFingerprint);
return values;
}
@@ -304,6 +322,14 @@ public class Message extends AbstractEntity {
this.type = type;
}
+ public boolean isCarbon() {
+ return carbon;
+ }
+
+ public void setCarbon(boolean carbon) {
+ this.carbon = carbon;
+ }
+
public void setTrueCounterpart(Jid trueCounterpart) {
this.trueCounterpart = trueCounterpart;
}
@@ -389,7 +415,8 @@ public class Message extends AbstractEntity {
message.treatAsDownloadable() == Decision.NEVER &&
this.treatAsDownloadable() == Decision.NEVER &&
!message.getBody().startsWith(ME_COMMAND) &&
- !this.getBody().startsWith(ME_COMMAND)
+ !this.getBody().startsWith(ME_COMMAND) &&
+ this.isTrusted() == message.isTrusted()
);
}
@@ -405,11 +432,14 @@ public class Message extends AbstractEntity {
}
public String getMergedBody() {
- final Message next = this.next();
- if (this.mergeable(next)) {
- return getBody() + MERGE_SEPARATOR + next.getMergedBody();
+ StringBuilder body = new StringBuilder(this.body.trim());
+ Message current = this;
+ while(current.mergeable(current.next())) {
+ current = current.next();
+ body.append(MERGE_SEPARATOR);
+ body.append(current.getBody());
}
- return getBody();
+ return body.toString();
}
public boolean hasMeCommand() {
@@ -417,20 +447,23 @@ public class Message extends AbstractEntity {
}
public int getMergedStatus() {
- final Message next = this.next();
- if (this.mergeable(next)) {
- return next.getStatus();
+ int status = this.status;
+ Message current = this;
+ while(current.mergeable(current.next())) {
+ current = current.next();
+ status = current.status;
}
- return getStatus();
+ return status;
}
public long getMergedTimeSent() {
- Message next = this.next();
- if (this.mergeable(next)) {
- return next.getMergedTimeSent();
- } else {
- return getTimeSent();
+ long time = this.timeSent;
+ Message current = this;
+ while(current.mergeable(current.next())) {
+ current = current.next();
+ time = current.timeSent;
}
+ return time;
}
public boolean wasMergedIntoPrevious() {
@@ -673,4 +706,48 @@ public class Message extends AbstractEntity {
public int width = 0;
public int height = 0;
}
+
+ public void setAxolotlFingerprint(String fingerprint) {
+ this.axolotlFingerprint = fingerprint;
+ }
+
+ public String getAxolotlFingerprint() {
+ return axolotlFingerprint;
+ }
+
+ public boolean isTrusted() {
+ return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint)
+ == XmppAxolotlSession.Trust.TRUSTED;
+ }
+
+ private int getPreviousEncryption() {
+ for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
+ if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
+ continue;
+ }
+ return iterator.getEncryption();
+ }
+ return ENCRYPTION_NONE;
+ }
+
+ private int getNextEncryption() {
+ for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
+ if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
+ continue;
+ }
+ return iterator.getEncryption();
+ }
+ return conversation.getNextEncryption();
+ }
+
+ public boolean isValidInSession() {
+ int pastEncryption = this.getPreviousEncryption();
+ int futureEncryption = this.getNextEncryption();
+
+ boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
+ || futureEncryption == ENCRYPTION_NONE
+ || pastEncryption != futureEncryption;
+
+ return inUnencryptedSession || this.getEncryption() == pastEncryption;
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
index d867a370..52a862ef 100644
--- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java
+++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.entities;
+import android.annotation.SuppressLint;
+
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -11,8 +13,6 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
-import android.annotation.SuppressLint;
-
@SuppressLint("DefaultLocale")
public class MucOptions {
@@ -264,6 +264,15 @@ public class MucOptions {
users.add(user);
}
+ public boolean isUserInRoom(String name) {
+ for (int i = 0; i < users.size(); ++i) {
+ if (users.get(i).getName().equals(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public void processPacket(PresencePacket packet, PgpEngine pgp) {
final Jid from = packet.getFrom();
if (!from.isBareJid()) {
diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
index 16984910..3a256219 100644
--- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
@@ -13,6 +13,7 @@ import java.util.Locale;
import java.util.TimeZone;
import de.tzur.conversations.Settings;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.PhoneHelper;
@@ -29,7 +30,8 @@ public abstract class AbstractGenerator {
"urn:xmpp:avatar:metadata+notify",
"urn:xmpp:ping",
"jabber:iq:version",
- "http://jabber.org/protocol/chatstates"};
+ "http://jabber.org/protocol/chatstates",
+ AxolotlService.PEP_DEVICE_LIST+"+notify"};
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
"urn:xmpp:chat-markers:0",
"urn:xmpp:receipts"
diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
index 47915e3f..898d218e 100644
--- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
@@ -1,15 +1,23 @@
package eu.siacs.conversations.generator;
+import android.util.Base64;
+
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.ecc.ECPublicKey;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.forms.Data;
@@ -115,6 +123,56 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
+ public IqPacket retrieveDeviceIds(final Jid to) {
+ final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
+ if(to != null) {
+ packet.setTo(to);
+ }
+ return packet;
+ }
+
+ public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) {
+ final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null);
+ if(to != null) {
+ packet.setTo(to);
+ }
+ return packet;
+ }
+
+ public IqPacket publishDeviceIds(final Set<Integer> ids) {
+ final Element item = new Element("item");
+ final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
+ for(Integer id:ids) {
+ final Element device = new Element("device");
+ device.setAttribute("id", id);
+ list.addChild(device);
+ }
+ return publish(AxolotlService.PEP_DEVICE_LIST, item);
+ }
+
+ public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
+ final Set<PreKeyRecord> preKeyRecords, final int deviceId) {
+ final Element item = new Element("item");
+ final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
+ final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
+ signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
+ ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
+ signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT));
+ final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
+ signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT));
+ final Element identityKeyElement = bundle.addChild("identityKey");
+ identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
+
+ final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
+ for(PreKeyRecord preKeyRecord:preKeyRecords) {
+ final Element prekey = prekeys.addChild("preKeyPublic");
+ prekey.setAttribute("preKeyId", preKeyRecord.getId());
+ prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT));
+ }
+
+ return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item);
+ }
+
public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
final Element query = packet.query("urn:xmpp:mam:0");
@@ -196,12 +254,15 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file) {
+ public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host);
Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD);
request.addChild("filename").setContent(file.getName());
request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
+ if (mime != null) {
+ request.addChild("content-type", mime);
+ }
return packet;
}
}
diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
index e698c151..a06a0ddd 100644
--- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
@@ -1,12 +1,14 @@
package eu.siacs.conversations.generator;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
-import net.java.otr4j.OtrException;
-import net.java.otr4j.session.Session;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
@@ -21,7 +23,7 @@ public class MessageGenerator extends AbstractGenerator {
super(service);
}
- private MessagePacket preparePacket(Message message, boolean addDelay) {
+ private MessagePacket preparePacket(Message message) {
Conversation conversation = message.getConversation();
Account account = conversation.getAccount();
MessagePacket packet = new MessagePacket();
@@ -44,13 +46,10 @@ public class MessageGenerator extends AbstractGenerator {
}
packet.setFrom(account.getJid());
packet.setId(message.getUuid());
- if (addDelay) {
- addDelay(packet, message.getTimeSent());
- }
return packet;
}
- private void addDelay(MessagePacket packet, long timestamp) {
+ public void addDelay(MessagePacket packet, long timestamp) {
final SimpleDateFormat mDateFormat = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
@@ -59,16 +58,21 @@ public class MessageGenerator extends AbstractGenerator {
delay.setAttribute("stamp", mDateFormat.format(date));
}
- public MessagePacket generateOtrChat(Message message) {
- return generateOtrChat(message, false);
+ public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
+ MessagePacket packet = preparePacket(message);
+ if (axolotlMessage == null) {
+ return null;
+ }
+ packet.setAxolotlMessage(axolotlMessage.toElement());
+ return packet;
}
- public MessagePacket generateOtrChat(Message message, boolean addDelay) {
+ public MessagePacket generateOtrChat(Message message) {
Session otrSession = message.getConversation().getOtrSession();
if (otrSession == null) {
return null;
}
- MessagePacket packet = preparePacket(message, addDelay);
+ MessagePacket packet = preparePacket(message);
packet.addChild("private", "urn:xmpp:carbons:2");
packet.addChild("no-copy", "urn:xmpp:hints");
packet.addChild("no-permanent-store", "urn:xmpp:hints");
@@ -88,11 +92,7 @@ public class MessageGenerator extends AbstractGenerator {
}
public MessagePacket generateChat(Message message) {
- return generateChat(message, false);
- }
-
- public MessagePacket generateChat(Message message, boolean addDelay) {
- MessagePacket packet = preparePacket(message, addDelay);
+ MessagePacket packet = preparePacket(message);
if (message.hasFileOnRemoteHost()) {
packet.setBody(message.getFileParams().url.toString());
} else {
@@ -102,11 +102,7 @@ public class MessageGenerator extends AbstractGenerator {
}
public MessagePacket generatePgpChat(Message message) {
- return generatePgpChat(message, false);
- }
-
- public MessagePacket generatePgpChat(Message message, boolean addDelay) {
- MessagePacket packet = preparePacket(message, addDelay);
+ MessagePacket packet = preparePacket(message);
packet.setBody("This is an XEP-0027 encrypted message");
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
@@ -119,25 +115,26 @@ public class MessageGenerator extends AbstractGenerator {
public MessagePacket generateChatState(Conversation conversation) {
final Account account = conversation.getAccount();
MessagePacket packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_CHAT);
packet.setTo(conversation.getJid().toBareJid());
packet.setFrom(account.getJid());
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
+ packet.addChild("no-store", "urn:xmpp:hints");
+ packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store*
return packet;
}
public MessagePacket confirm(final Account account, final Jid to, final String id) {
MessagePacket packet = new MessagePacket();
- packet.setType(MessagePacket.TYPE_NORMAL);
+ packet.setType(MessagePacket.TYPE_CHAT);
packet.setTo(to);
packet.setFrom(account.getJid());
- Element received = packet.addChild("displayed",
- "urn:xmpp:chat-markers:0");
+ Element received = packet.addChild("displayed","urn:xmpp:chat-markers:0");
received.setAttribute("id", id);
return packet;
}
- public MessagePacket conferenceSubject(Conversation conversation,
- String subject) {
+ public MessagePacket conferenceSubject(Conversation conversation,String subject) {
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_GROUPCHAT);
packet.setTo(conversation.getJid().toBareJid());
@@ -171,10 +168,9 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
- public MessagePacket received(Account account,
- MessagePacket originalMessage, String namespace) {
+ public MessagePacket received(Account account, MessagePacket originalMessage, String namespace, int type) {
MessagePacket receivedPacket = new MessagePacket();
- receivedPacket.setType(MessagePacket.TYPE_NORMAL);
+ receivedPacket.setType(type);
receivedPacket.setTo(originalMessage.getFrom());
receivedPacket.setFrom(account.getJid());
Element received = receivedPacket.addChild("received", namespace);
diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
index 58a6d1e3..90fbadfe 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
@@ -38,9 +38,9 @@ public class HttpConnectionManager extends AbstractConnectionManager {
return connection;
}
- public HttpUploadConnection createNewUploadConnection(Message message) {
+ public HttpUploadConnection createNewUploadConnection(Message message, boolean delay) {
HttpUploadConnection connection = new HttpUploadConnection(this);
- connection.init(message);
+ connection.init(message,delay);
this.uploadConnections.add(connection);
return connection;
}
diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
index 45f1e965..356f9417 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
@@ -2,11 +2,12 @@ package eu.siacs.conversations.http;
import android.content.Intent;
import android.net.Uri;
-import android.os.SystemClock;
+import android.os.PowerManager;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
@@ -18,9 +19,11 @@ import javax.net.ssl.SSLHandshakeException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -35,7 +38,6 @@ public class HttpDownloadConnection implements Transferable {
private int mStatus = Transferable.STATUS_UNKNOWN;
private boolean acceptedAutomatically = false;
private int mProgress = 0;
- private long mLastGuiRefresh = 0;
public HttpDownloadConnection(HttpConnectionManager manager) {
this.mHttpConnectionManager = manager;
@@ -70,7 +72,8 @@ public class HttpDownloadConnection implements Transferable {
String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null;
if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
this.message.setEncryption(Message.ENCRYPTION_PGP);
- } else if (message.getEncryption() != Message.ENCRYPTION_OTR) {
+ } else if (message.getEncryption() != Message.ENCRYPTION_OTR
+ && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
String extension;
@@ -83,10 +86,11 @@ public class HttpDownloadConnection implements Transferable {
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
String reference = mUrl.getRef();
if (reference != null && reference.length() == 96) {
- this.file.setKey(CryptoHelper.hexToBytes(reference));
+ this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
}
- if (this.message.getEncryption() == Message.ENCRYPTION_OTR
+ if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
+ || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
&& this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
@@ -123,6 +127,17 @@ public class HttpDownloadConnection implements Transferable {
mXmppConnectionService.updateConversationUi();
}
+ private void showToastForException(Exception e) {
+ e.printStackTrace();
+ if (e instanceof java.net.UnknownHostException) {
+ mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
+ } else if (e instanceof java.net.ConnectException) {
+ mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
+ } else {
+ mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
+ }
+ }
+
private class FileSizeChecker implements Runnable {
private boolean interactive = false;
@@ -144,7 +159,7 @@ public class HttpDownloadConnection implements Transferable {
} catch (IOException e) {
Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
if (interactive) {
- mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host);
+ showToastForException(e);
}
cancel();
return;
@@ -186,6 +201,8 @@ public class HttpDownloadConnection implements Transferable {
private boolean interactive = false;
+ private OutputStream os;
+
public FileDownloader(boolean interactive) {
this.interactive = interactive;
}
@@ -200,36 +217,44 @@ public class HttpDownloadConnection implements Transferable {
} catch (SSLHandshakeException e) {
changeStatus(STATUS_OFFER);
} catch (IOException e) {
- mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host);
+ if (interactive) {
+ showToastForException(e);
+ }
cancel();
}
}
- private void download() throws SSLHandshakeException, IOException {
- HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
- if (connection instanceof HttpsURLConnection) {
- mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
- }
- connection.connect();
- BufferedInputStream is = new BufferedInputStream(connection.getInputStream());
- file.getParentFile().mkdirs();
- file.createNewFile();
- OutputStream os = file.createOutputStream();
- if (os == null) {
- throw new IOException();
- }
- long transmitted = 0;
- long expected = file.getExpectedSize();
- int count = -1;
- byte[] buffer = new byte[1024];
- while ((count = is.read(buffer)) != -1) {
- transmitted += count;
- os.write(buffer, 0, count);
- updateProgress((int) ((((double) transmitted) / expected) * 100));
+ private void download() throws IOException {
+ InputStream is = null;
+ PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid());
+ try {
+ wakeLock.acquire();
+ HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
+ if (connection instanceof HttpsURLConnection) {
+ mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
+ }
+ connection.connect();
+ is = new BufferedInputStream(connection.getInputStream());
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ os = AbstractConnectionManager.createOutputStream(file, true);
+ long transmitted = 0;
+ long expected = file.getExpectedSize();
+ int count = -1;
+ byte[] buffer = new byte[1024];
+ while ((count = is.read(buffer)) != -1) {
+ transmitted += count;
+ os.write(buffer, 0, count);
+ updateProgress((int) ((((double) transmitted) / expected) * 100));
+ }
+ os.flush();
+ } catch (IOException e) {
+ throw e;
+ } finally {
+ FileBackend.close(os);
+ FileBackend.close(is);
+ wakeLock.release();
}
- os.flush();
- os.close();
- is.close();
}
private void updateImageBounds() {
@@ -242,10 +267,7 @@ public class HttpDownloadConnection implements Transferable {
public void updateProgress(int i) {
this.mProgress = i;
- if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
- this.mLastGuiRefresh = SystemClock.elapsedRealtime();
- mXmppConnectionService.updateConversationUi();
- }
+ mXmppConnectionService.updateConversationUi();
}
@Override
diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
index a3ab8dab..2e545842 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
@@ -1,8 +1,13 @@
package eu.siacs.conversations.http;
import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.PowerManager;
import android.util.Log;
+import android.util.Pair;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -14,10 +19,11 @@ import javax.net.ssl.HttpsURLConnection;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -33,16 +39,19 @@ public class HttpUploadConnection implements Transferable {
private XmppConnectionService mXmppConnectionService;
private boolean canceled = false;
+ private boolean delayed = false;
private Account account;
private DownloadableFile file;
private Message message;
+ private String mime;
private URL mGetUrl;
private URL mPutUrl;
private byte[] key = null;
private long transmitted = 0;
- private long expected = 1;
+
+ private InputStream mFileInputStream;
public HttpUploadConnection(HttpConnectionManager httpConnectionManager) {
this.mHttpConnectionManager = httpConnectionManager;
@@ -66,7 +75,7 @@ public class HttpUploadConnection implements Transferable {
@Override
public int getProgress() {
- return (int) ((((double) transmitted) / expected) * 100);
+ return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
}
@Override
@@ -77,25 +86,36 @@ public class HttpUploadConnection implements Transferable {
private void fail() {
mHttpConnectionManager.finishUploadConnection(this);
message.setTransferable(null);
- mXmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED);
+ mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
+ FileBackend.close(mFileInputStream);
}
- public void init(Message message) {
+ public void init(Message message, boolean delay) {
this.message = message;
message.setTransferable(this);
- mXmppConnectionService.markMessage(message,Message.STATUS_UNSEND);
+ mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
this.account = message.getConversation().getAccount();
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
- this.file.setExpectedSize(this.file.getSize());
-
- if (Config.ENCRYPT_ON_HTTP_UPLOADED) {
+ this.mime = this.file.getMimeType();
+ this.delayed = delay;
+ if (Config.ENCRYPT_ON_HTTP_UPLOADED
+ || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
+ || message.getEncryption() == Message.ENCRYPTION_OTR) {
this.key = new byte[48];
mXmppConnectionService.getRNG().nextBytes(this.key);
- this.file.setKey(this.key);
+ this.file.setKeyAndIv(this.key);
}
-
+ Pair<InputStream,Integer> pair;
+ try {
+ pair = AbstractConnectionManager.createInputStream(file, true);
+ } catch (FileNotFoundException e) {
+ fail();
+ return;
+ }
+ this.file.setExpectedSize(pair.second);
+ this.mFileInputStream = pair.first;
Jid host = account.getXmppConnection().findDiscoItemByFeature(Xmlns.HTTP_UPLOAD);
- IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file);
+ IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file,mime);
mXmppConnectionService.sendIqPacket(account, request, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
@@ -130,9 +150,10 @@ public class HttpUploadConnection implements Transferable {
private void upload() {
OutputStream os = null;
- InputStream is = null;
HttpURLConnection connection = null;
+ PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_upload_"+message.getUuid());
try {
+ wakeLock.acquire();
Log.d(Config.LOGTAG, "uploading to " + mPutUrl.toString());
connection = (HttpURLConnection) mPutUrl.openConnection();
if (connection instanceof HttpsURLConnection) {
@@ -140,37 +161,38 @@ public class HttpUploadConnection implements Transferable {
}
connection.setRequestMethod("PUT");
connection.setFixedLengthStreamingMode((int) file.getExpectedSize());
+ connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime);
connection.setDoOutput(true);
connection.connect();
os = connection.getOutputStream();
- is = file.createInputStream();
transmitted = 0;
- expected = file.getExpectedSize();
int count = -1;
byte[] buffer = new byte[4096];
- while (((count = is.read(buffer)) != -1) && !canceled) {
+ while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) {
transmitted += count;
os.write(buffer, 0, count);
mXmppConnectionService.updateConversationUi();
}
os.flush();
os.close();
- is.close();
+ mFileInputStream.close();
int code = connection.getResponseCode();
if (code == 200 || code == 201) {
Log.d(Config.LOGTAG, "finished uploading file");
- Message.FileParams params = message.getFileParams();
if (key != null) {
mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key));
}
mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl);
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ mXmppConnectionService.sendBroadcast(intent);
message.setTransferable(null);
message.setCounterpart(message.getConversation().getJid().toBareJid());
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
mXmppConnectionService.getPgpEngine().encrypt(message, new UiCallback<Message>() {
@Override
public void success(Message message) {
- mXmppConnectionService.resendMessage(message);
+ mXmppConnectionService.resendMessage(message,delayed);
}
@Override
@@ -184,20 +206,22 @@ public class HttpUploadConnection implements Transferable {
}
});
} else {
- mXmppConnectionService.resendMessage(message);
+ mXmppConnectionService.resendMessage(message, delayed);
}
} else {
fail();
}
} catch (IOException e) {
- Log.d(Config.LOGTAG, e.getMessage());
+ e.printStackTrace();
+ Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
fail();
} finally {
- FileBackend.close(is);
+ FileBackend.close(mFileInputStream);
FileBackend.close(os);
if (connection != null) {
connection.disconnect();
}
+ wakeLock.release();
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
index 11ddcc13..3f11f756 100644
--- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java
@@ -76,11 +76,9 @@ public abstract class AbstractParser {
return dateFormat.parse(timestamp);
}
- protected void updateLastseen(final Element packet, final Account account,
- final boolean presenceOverwrite) {
- final Jid from = packet.getAttributeAsJid("from");
- updateLastseen(packet, account, from, presenceOverwrite);
- }
+ protected void updateLastseen(final Element packet, final Account account, final boolean presenceOverwrite) {
+ updateLastseen(packet, account, packet.getAttributeAsJid("from"), presenceOverwrite);
+ }
protected void updateLastseen(final Element packet, final Account account, final Jid from, final boolean presenceOverwrite) {
final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart();
diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java
index 6039d395..cef5b03d 100644
--- a/src/main/java/eu/siacs/conversations/parser/IqParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java
@@ -1,11 +1,25 @@
package eu.siacs.conversations.parser;
+import android.support.annotation.NonNull;
+import android.util.Base64;
import android.util.Log;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.InvalidKeyException;
+import org.whispersystems.libaxolotl.ecc.Curve;
+import org.whispersystems.libaxolotl.ecc.ECPublicKey;
+import org.whispersystems.libaxolotl.state.PreKeyBundle;
+
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
@@ -71,9 +85,160 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
return super.avatarData(items);
}
+ public Element getItem(final IqPacket packet) {
+ final Element pubsub = packet.findChild("pubsub",
+ "http://jabber.org/protocol/pubsub");
+ if (pubsub == null) {
+ return null;
+ }
+ final Element items = pubsub.findChild("items");
+ if (items == null) {
+ return null;
+ }
+ return items.findChild("item");
+ }
+
+ @NonNull
+ public Set<Integer> deviceIds(final Element item) {
+ Set<Integer> deviceIds = new HashSet<>();
+ if (item != null) {
+ final Element list = item.findChild("list");
+ if (list != null) {
+ for (Element device : list.getChildren()) {
+ if (!device.getName().equals("device")) {
+ continue;
+ }
+ try {
+ Integer id = Integer.valueOf(device.getAttribute("id"));
+ deviceIds.add(id);
+ } catch (NumberFormatException e) {
+ Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered nvalid <device> node in PEP:" + device.toString()
+ + ", skipping...");
+ continue;
+ }
+ }
+ }
+ }
+ return deviceIds;
+ }
+
+ public Integer signedPreKeyId(final Element bundle) {
+ final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
+ if(signedPreKeyPublic == null) {
+ return null;
+ }
+ return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
+ }
+
+ public ECPublicKey signedPreKeyPublic(final Element bundle) {
+ ECPublicKey publicKey = null;
+ final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
+ if(signedPreKeyPublic == null) {
+ return null;
+ }
+ try {
+ publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0);
+ } catch (InvalidKeyException e) {
+ Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage());
+ }
+ return publicKey;
+ }
+
+ public byte[] signedPreKeySignature(final Element bundle) {
+ final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature");
+ if(signedPreKeySignature == null) {
+ return null;
+ }
+ return Base64.decode(signedPreKeySignature.getContent(),Base64.DEFAULT);
+ }
+
+ public IdentityKey identityKey(final Element bundle) {
+ IdentityKey identityKey = null;
+ final Element identityKeyElement = bundle.findChild("identityKey");
+ if(identityKeyElement == null) {
+ return null;
+ }
+ try {
+ identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
+ } catch (InvalidKeyException e) {
+ Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
+ }
+ return identityKey;
+ }
+
+ public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
+ Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
+ Element item = getItem(packet);
+ if (item == null) {
+ Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet);
+ return null;
+ }
+ final Element bundleElement = item.findChild("bundle");
+ if(bundleElement == null) {
+ return null;
+ }
+ final Element prekeysElement = bundleElement.findChild("prekeys");
+ if(prekeysElement == null) {
+ Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet);
+ return null;
+ }
+ for(Element preKeyPublicElement : prekeysElement.getChildren()) {
+ if(!preKeyPublicElement.getName().equals("preKeyPublic")){
+ Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
+ continue;
+ }
+ Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
+ try {
+ ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
+ preKeyRecords.put(preKeyId, preKeyPublic);
+ } catch (InvalidKeyException e) {
+ Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
+ continue;
+ }
+ }
+ return preKeyRecords;
+ }
+
+ public PreKeyBundle bundle(final IqPacket bundle) {
+ Element bundleItem = getItem(bundle);
+ if(bundleItem == null) {
+ return null;
+ }
+ final Element bundleElement = bundleItem.findChild("bundle");
+ if(bundleElement == null) {
+ return null;
+ }
+ ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
+ Integer signedPreKeyId = signedPreKeyId(bundleElement);
+ byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
+ IdentityKey identityKey = identityKey(bundleElement);
+ if(signedPreKeyPublic == null || identityKey == null) {
+ return null;
+ }
+
+ return new PreKeyBundle(0, 0, 0, null,
+ signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
+ }
+
+ public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
+ List<PreKeyBundle> bundles = new ArrayList<>();
+ Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
+ if ( preKeyPublics != null) {
+ for (Integer preKeyId : preKeyPublics.keySet()) {
+ ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
+ bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
+ 0, null, null, null));
+ }
+ }
+
+ return bundles;
+ }
+
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
- if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {
+ if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) {
+ return;
+ } else if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {
final Element query = packet.findChild("query");
// If this is in response to a query for the whole roster:
if (packet.getType() == IqPacket.TYPE.RESULT) {
@@ -143,15 +308,13 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null);
} else {
- if ((packet.getType() == IqPacket.TYPE.GET)
- || (packet.getType() == IqPacket.TYPE.SET)) {
+ if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error");
error.setAttribute("type", "cancel");
- error.addChild("feature-not-implemented",
- "urn:ietf:params:xml:ns:xmpp-stanzas");
+ error.addChild("feature-not-implemented","urn:ietf:params:xml:ns:xmpp-stanzas");
account.getXmppConnection().sendIqPacket(response, null);
- }
+ }
}
}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index 81fd89cd..e2f13bd3 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -6,8 +6,12 @@ import android.util.Pair;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
+import java.util.Set;
+
import de.tzur.conversations.Settings;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@@ -70,11 +74,9 @@ public class MessageParser extends AbstractParser implements
body = otrSession.transformReceiving(body);
SessionStatus status = otrSession.getSessionStatus();
if (body == null && status == SessionStatus.ENCRYPTED) {
- conversation.setNextEncryption(Message.ENCRYPTION_OTR);
mXmppConnectionService.onOtrSessionEstablished(conversation);
return null;
} else if (body == null && status == SessionStatus.FINISHED) {
- conversation.setNextEncryption(Message.ENCRYPTION_NONE);
conversation.resetOtrSession();
mXmppConnectionService.updateConversationUi();
return null;
@@ -95,6 +97,20 @@ public class MessageParser extends AbstractParser implements
}
}
+ private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) {
+ Message finishedMessage = null;
+ AxolotlService service = conversation.getAccount().getAxolotlService();
+ XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
+ XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage);
+ if(plaintextMessage != null) {
+ finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
+ finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint());
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
+ }
+
+ return finishedMessage;
+ }
+
private class Invite {
Jid jid;
String password;
@@ -102,6 +118,8 @@ public class MessageParser extends AbstractParser implements
this.jid = jid;
this.password = password;
}
+ return false;
+ }
public boolean execute(Account account) {
if (jid != null) {
@@ -171,6 +189,13 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateAccountUi();
}
+ } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing...");
+ Element item = items.findChild("item");
+ Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+ AxolotlService axolotlService = account.getAxolotlService();
+ axolotlService.registerDevices(from, deviceIds);
+ mXmppConnectionService.updateAccountUi();
}
}
@@ -178,6 +203,13 @@ public class MessageParser extends AbstractParser implements
if (packet.getType() == MessagePacket.TYPE_ERROR) {
Jid from = packet.getFrom();
if (from != null) {
+ Element error = packet.findChild("error");
+ String text = error == null ? null : error.findChildContent("text");
+ if (text != null) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + text);
+ } else if (error != null) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error);
+ }
Message message = mXmppConnectionService.markMessage(account,
from.toBareJid(),
packet.getId(),
@@ -199,6 +231,7 @@ public class MessageParser extends AbstractParser implements
final MessagePacket packet;
Long timestamp = null;
final boolean isForwarded;
+ boolean isCarbon = false;
String serverMsgId = null;
final Element fin = original.findChild("fin", "urn:xmpp:mam:0");
if (fin != null) {
@@ -229,7 +262,8 @@ public class MessageParser extends AbstractParser implements
return;
}
timestamp = f != null ? f.second : null;
- isForwarded = f != null;
+ isCarbon = f != null;
+ isForwarded = isCarbon;
} else {
packet = original;
isForwarded = false;
@@ -239,8 +273,9 @@ public class MessageParser extends AbstractParser implements
timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
}
final String body = packet.getBody();
- final String encrypted = packet.findChildContent("x", "jabber:x:encrypted");
- final Element mucUserElement = packet.findChild("x","http://jabber.org/protocol/muc#user");
+ final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
+ final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
+ final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status;
final Jid counterpart;
final Jid to = packet.getTo();
@@ -268,11 +303,11 @@ public class MessageParser extends AbstractParser implements
return;
}
- if (extractChatState(mXmppConnectionService.find(account,from), packet)) {
+ if (extractChatState(mXmppConnectionService.find(account, from), packet)) {
mXmppConnectionService.updateConversationUi();
}
- if ((body != null || encrypted != null) && !isMucStatusMessage) {
+ if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) {
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat);
if (isTypeGroupChat) {
if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
@@ -301,14 +336,20 @@ public class MessageParser extends AbstractParser implements
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
- } else if (encrypted != null) {
- message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status);
+ } else if (pgpEncrypted != null) {
+ message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
+ } else if (axolotlEncrypted != null) {
+ message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
+ if (message == null) {
+ return;
+ }
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
message.setCounterpart(counterpart);
message.setRemoteMsgId(remoteMsgId);
message.setServerMsgId(serverMsgId);
+ message.setCarbon(isCarbon);
message.setTime(timestamp);
message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
if (conversation.getMode() == Conversation.MODE_MULTI) {
@@ -339,15 +380,19 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.updateConversationUi();
}
- if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded) {
+ if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
- MessagePacket receipt = mXmppConnectionService
- .getMessageGenerator().received(account, packet, "urn:xmpp:chat-markers:0");
+ MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
+ packet,
+ "urn:xmpp:chat-markers:0",
+ MessagePacket.TYPE_CHAT);
mXmppConnectionService.sendMessagePacket(account, receipt);
}
if (packet.hasChild("request", "urn:xmpp:receipts")) {
- MessagePacket receipt = mXmppConnectionService
- .getMessageGenerator().received(account, packet, "urn:xmpp:receipts");
+ MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
+ packet,
+ "urn:xmpp:receipts",
+ packet.getType());
mXmppConnectionService.sendMessagePacket(account, receipt);
}
}
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
index d11b02fa..9fe47512 100644
--- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -1,10 +1,34 @@
package eu.siacs.conversations.persistance;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Base64;
+import android.util.Log;
+
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.IdentityKeyPair;
+import org.whispersystems.libaxolotl.InvalidKeyException;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SessionRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+
+import java.io.IOException;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
+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;
@@ -13,19 +37,12 @@ import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteCantOpenDatabaseException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.util.Log;
-
public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history";
- private static final int DATABASE_VERSION = 14;
+ private static final int DATABASE_VERSION = 16;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -33,12 +50,66 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Contact.JID + " TEXT," + Contact.KEYS + " TEXT,"
+ Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER,"
+ Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, "
- + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, "
+ + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, "
+ Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
+ Account.TABLENAME + "(" + Account.UUID
+ ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+ Contact.JID + ") ON CONFLICT REPLACE);";
+ private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
+ + SQLiteAxolotlStore.PREKEY_TABLENAME + "("
+ + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ + SQLiteAxolotlStore.ID + " INTEGER, "
+ + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ + SQLiteAxolotlStore.ACCOUNT
+ + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ + SQLiteAxolotlStore.ID
+ + ") ON CONFLICT REPLACE"
+ +");";
+
+ private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE "
+ + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "("
+ + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ + SQLiteAxolotlStore.ID + " INTEGER, "
+ + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ + SQLiteAxolotlStore.ACCOUNT
+ + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ + SQLiteAxolotlStore.ID
+ + ") ON CONFLICT REPLACE"+
+ ");";
+
+ private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE "
+ + SQLiteAxolotlStore.SESSION_TABLENAME + "("
+ + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ + SQLiteAxolotlStore.NAME + " TEXT, "
+ + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, "
+ + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ + SQLiteAxolotlStore.ACCOUNT
+ + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ + SQLiteAxolotlStore.NAME + ", "
+ + SQLiteAxolotlStore.DEVICE_ID
+ + ") ON CONFLICT REPLACE"
+ +");";
+
+ private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE "
+ + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "("
+ + SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ + SQLiteAxolotlStore.NAME + " TEXT, "
+ + SQLiteAxolotlStore.OWN + " INTEGER, "
+ + SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
+ + SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ + SQLiteAxolotlStore.ACCOUNT
+ + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ + SQLiteAxolotlStore.NAME + ", "
+ + SQLiteAxolotlStore.FINGERPRINT
+ + ") ON CONFLICT IGNORE"
+ +");";
+
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@@ -69,12 +140,18 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
+ Message.RELATIVE_FILE_PATH + " TEXT, "
+ Message.SERVER_MSG_ID + " TEXT, "
+ + Message.FINGERPRINT + " TEXT, "
+ + Message.CARBON + " INTEGER, "
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
+ ") ON DELETE CASCADE);");
db.execSQL(CREATE_CONTATCS_STATEMENT);
+ db.execSQL(CREATE_SESSIONS_STATEMENT);
+ db.execSQL(CREATE_PREKEYS_STATEMENT);
+ db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
+ db.execSQL(CREATE_IDENTITIES_STATEMENT);
}
@Override
@@ -109,12 +186,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN "
+ Conversation.ATTRIBUTES + " TEXT");
}
- if (oldVersion < 9 && newVersion >= 9) {
- db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
- + Contact.LAST_TIME + " NUMBER");
- db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
- + Contact.LAST_PRESENCE + " TEXT");
- }
+ if (oldVersion < 9 && newVersion >= 9) {
+ db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ + Contact.LAST_TIME + " NUMBER");
+ db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
+ + Contact.LAST_PRESENCE + " TEXT");
+ }
if (oldVersion < 10 && newVersion >= 10) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ Message.RELATIVE_FILE_PATH + " TEXT");
@@ -215,6 +292,15 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
cursor.close();
}
+ if (oldVersion < 15 && newVersion >= 15) {
+ recreateAxolotlDb(db);
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.FINGERPRINT + " TEXT");
+ }
+ if (oldVersion < 16 && newVersion >= 16) {
+ db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ + Message.CARBON + " INTEGER");
+ }
}
public static synchronized DatabaseBackend getInstance(Context context) {
@@ -311,7 +397,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
};
Cursor cursor = db.query(Conversation.TABLENAME, null,
Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
- + " like ? OR "+Conversation.CONTACTJID+"=?)", selectionArgs, null, null, null);
+ + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
if (cursor.getCount() == 0)
return null;
cursor.moveToFirst();
@@ -481,4 +567,405 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cursor.close();
return list;
}
+
+ private Cursor getCursorForSession(Account account, AxolotlAddress contact) {
+ final SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = null;
+ String[] selectionArgs = {account.getUuid(),
+ contact.getName(),
+ Integer.toString(contact.getDeviceId())};
+ Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.NAME + " = ? AND "
+ + SQLiteAxolotlStore.DEVICE_ID + " = ? ",
+ selectionArgs,
+ null, null, null);
+
+ return cursor;
+ }
+
+ public SessionRecord loadSession(Account account, AxolotlAddress contact) {
+ SessionRecord session = null;
+ Cursor cursor = getCursorForSession(account, contact);
+ if(cursor.getCount() != 0) {
+ cursor.moveToFirst();
+ try {
+ session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
+ } catch (IOException e) {
+ cursor.close();
+ throw new AssertionError(e);
+ }
+ }
+ cursor.close();
+ return session;
+ }
+
+ public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) {
+ List<Integer> devices = new ArrayList<>();
+ final SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = {SQLiteAxolotlStore.DEVICE_ID};
+ String[] selectionArgs = {account.getUuid(),
+ contact.getName()};
+ Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.NAME + " = ?",
+ selectionArgs,
+ null, null, null);
+
+ while(cursor.moveToNext()) {
+ devices.add(cursor.getInt(
+ cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID)));
+ }
+
+ cursor.close();
+ return devices;
+ }
+
+ public boolean containsSession(Account account, AxolotlAddress contact) {
+ Cursor cursor = getCursorForSession(account, contact);
+ int count = cursor.getCount();
+ cursor.close();
+ return count != 0;
+ }
+
+ public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.NAME, contact.getName());
+ values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId());
+ values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT));
+ values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+ db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values);
+ }
+
+ public void deleteSession(Account account, AxolotlAddress contact) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {account.getUuid(),
+ contact.getName(),
+ Integer.toString(contact.getDeviceId())};
+ db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.NAME + " = ? AND "
+ + SQLiteAxolotlStore.DEVICE_ID + " = ? ",
+ args);
+ }
+
+ public void deleteAllSessions(Account account, AxolotlAddress contact) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {account.getUuid(), contact.getName()};
+ db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ + SQLiteAxolotlStore.NAME + " = ?",
+ args);
+ }
+
+ private Cursor getCursorForPreKey(Account account, int preKeyId) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = {SQLiteAxolotlStore.KEY};
+ String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)};
+ Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ + SQLiteAxolotlStore.ID + "=?",
+ selectionArgs,
+ null, null, null);
+
+ return cursor;
+ }
+
+ public PreKeyRecord loadPreKey(Account account, int preKeyId) {
+ PreKeyRecord record = null;
+ Cursor cursor = getCursorForPreKey(account, preKeyId);
+ if(cursor.getCount() != 0) {
+ cursor.moveToFirst();
+ try {
+ record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
+ } catch (IOException e ) {
+ throw new AssertionError(e);
+ }
+ }
+ cursor.close();
+ return record;
+ }
+
+ public boolean containsPreKey(Account account, int preKeyId) {
+ Cursor cursor = getCursorForPreKey(account, preKeyId);
+ int count = cursor.getCount();
+ cursor.close();
+ return count != 0;
+ }
+
+ public void storePreKey(Account account, PreKeyRecord record) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.ID, record.getId());
+ values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
+ values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+ db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values);
+ }
+
+ public void deletePreKey(Account account, int preKeyId) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {account.getUuid(), Integer.toString(preKeyId)};
+ db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ + SQLiteAxolotlStore.ID + "=?",
+ args);
+ }
+
+ private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = {SQLiteAxolotlStore.KEY};
+ String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)};
+ Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
+ selectionArgs,
+ null, null, null);
+
+ return cursor;
+ }
+
+ public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) {
+ SignedPreKeyRecord record = null;
+ Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId);
+ if(cursor.getCount() != 0) {
+ cursor.moveToFirst();
+ try {
+ record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
+ } catch (IOException e ) {
+ throw new AssertionError(e);
+ }
+ }
+ cursor.close();
+ return record;
+ }
+
+ public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
+ List<SignedPreKeyRecord> prekeys = new ArrayList<>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = {SQLiteAxolotlStore.KEY};
+ String[] selectionArgs = {account.getUuid()};
+ Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ columns,
+ SQLiteAxolotlStore.ACCOUNT + "=?",
+ selectionArgs,
+ null, null, null);
+
+ while(cursor.moveToNext()) {
+ try {
+ prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)));
+ } catch (IOException ignored) {
+ }
+ }
+ cursor.close();
+ return prekeys;
+ }
+
+ public boolean containsSignedPreKey(Account account, int signedPreKeyId) {
+ Cursor cursor = getCursorForPreKey(account, signedPreKeyId);
+ int count = cursor.getCount();
+ cursor.close();
+ return count != 0;
+ }
+
+ public void storeSignedPreKey(Account account, SignedPreKeyRecord record) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.ID, record.getId());
+ values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
+ values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+ db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values);
+ }
+
+ public void deleteSignedPreKey(Account account, int signedPreKeyId) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)};
+ db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ + SQLiteAxolotlStore.ID + "=?",
+ args);
+ }
+
+ private Cursor getIdentityKeyCursor(Account account, String name, boolean own) {
+ return getIdentityKeyCursor(account, name, own, null);
+ }
+
+ private Cursor getIdentityKeyCursor(Account account, String fingerprint) {
+ return getIdentityKeyCursor(account, null, null, fingerprint);
+ }
+
+ private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) {
+ final SQLiteDatabase db = this.getReadableDatabase();
+ String[] columns = {SQLiteAxolotlStore.TRUSTED,
+ SQLiteAxolotlStore.KEY};
+ ArrayList<String> selectionArgs = new ArrayList<>(4);
+ selectionArgs.add(account.getUuid());
+ String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?";
+ if (name != null){
+ selectionArgs.add(name);
+ selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?";
+ }
+ if (fingerprint != null){
+ selectionArgs.add(fingerprint);
+ selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?";
+ }
+ if (own != null){
+ selectionArgs.add(own?"1":"0");
+ selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?";
+ }
+ Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+ columns,
+ selectionString,
+ selectionArgs.toArray(new String[selectionArgs.size()]),
+ null, null, null);
+
+ return cursor;
+ }
+
+ public IdentityKeyPair loadOwnIdentityKeyPair(Account account, String name) {
+ IdentityKeyPair identityKeyPair = null;
+ Cursor cursor = getIdentityKeyCursor(account, name, true);
+ if(cursor.getCount() != 0) {
+ cursor.moveToFirst();
+ try {
+ identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
+ }
+ }
+ cursor.close();
+
+ return identityKeyPair;
+ }
+
+ public Set<IdentityKey> loadIdentityKeys(Account account, String name) {
+ return loadIdentityKeys(account, name, null);
+ }
+
+ public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) {
+ Set<IdentityKey> identityKeys = new HashSet<>();
+ Cursor cursor = getIdentityKeyCursor(account, name, false);
+
+ while(cursor.moveToNext()) {
+ if ( trust != null &&
+ cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED))
+ != trust.getCode()) {
+ continue;
+ }
+ try {
+ identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0));
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name);
+ }
+ }
+ cursor.close();
+
+ return identityKeys;
+ }
+
+ public long numTrustedKeys(Account account, String name) {
+ SQLiteDatabase db = getReadableDatabase();
+ String[] args = {
+ account.getUuid(),
+ name,
+ String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode())
+ };
+ return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?"
+ + " AND " + SQLiteAxolotlStore.NAME + " = ?"
+ + " AND " + SQLiteAxolotlStore.TRUSTED + " = ?",
+ args
+ );
+ }
+
+ private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
+ storeIdentityKey(account, name, own, fingerprint, base64Serialized, XmppAxolotlSession.Trust.UNDECIDED);
+ }
+
+ private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, XmppAxolotlSession.Trust trusted) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+ values.put(SQLiteAxolotlStore.NAME, name);
+ values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
+ values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
+ values.put(SQLiteAxolotlStore.KEY, base64Serialized);
+ values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode());
+ db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
+ }
+
+ public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
+ Cursor cursor = getIdentityKeyCursor(account, fingerprint);
+ XmppAxolotlSession.Trust trust = null;
+ if (cursor.getCount() > 0) {
+ cursor.moveToFirst();
+ int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED));
+ trust = XmppAxolotlSession.Trust.fromCode(trustValue);
+ }
+ cursor.close();
+ return trust;
+ }
+
+ public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] selectionArgs = {
+ account.getUuid(),
+ fingerprint
+ };
+ ContentValues values = new ContentValues();
+ values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode());
+ int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
+ SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
+ selectionArgs);
+ return rows == 1;
+ }
+
+ public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
+ storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
+ }
+
+ public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) {
+ storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED);
+ }
+
+ public void recreateAxolotlDb() {
+ recreateAxolotlDb(getWritableDatabase());
+ }
+
+ public void recreateAxolotlDb(SQLiteDatabase db) {
+ Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<");
+ db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME);
+ db.execSQL(CREATE_SESSIONS_STATEMENT);
+ db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME);
+ db.execSQL(CREATE_PREKEYS_STATEMENT);
+ db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME);
+ db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
+ db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME);
+ db.execSQL(CREATE_IDENTITIES_STATEMENT);
+ }
+
+ public void wipeAxolotlDb(Account account) {
+ String accountName = account.getUuid();
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<");
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] deleteArgs= {
+ accountName
+ };
+ db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?",
+ deleteArgs);
+ db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?",
+ deleteArgs);
+ db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?",
+ deleteArgs);
+ db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+ SQLiteAxolotlStore.ACCOUNT + " = ?",
+ deleteArgs);
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
index ab191285..9381ba1d 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -1,5 +1,19 @@
package eu.siacs.conversations.persistance;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
@@ -8,6 +22,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.Socket;
import java.net.URL;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
@@ -17,28 +32,15 @@ import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.RectF;
-import android.net.Uri;
-import android.os.Environment;
-import android.provider.MediaStore;
-import android.util.Base64;
-import android.util.Base64OutputStream;
-import android.util.Log;
-import android.webkit.MimeTypeMap;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper;
+import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.xmpp.pep.Avatar;
public class FileBackend {
@@ -126,25 +128,24 @@ public class FileBackend {
return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
}
- public String getOriginalPath(Uri uri) {
- String path = null;
- if (uri.getScheme().equals("file")) {
- return uri.getPath();
- } else if (uri.toString().startsWith("content://media/")) {
- String[] projection = {MediaStore.MediaColumns.DATA};
- Cursor metaCursor = mXmppConnectionService.getContentResolver().query(uri,
- projection, null, null, null);
- if (metaCursor != null) {
- try {
- if (metaCursor.moveToFirst()) {
- path = metaCursor.getString(0);
- }
- } finally {
- metaCursor.close();
- }
- }
+ public boolean useImageAsIs(Uri uri) {
+ String path = getOriginalPath(uri);
+ if (path == null) {
+ return false;
+ }
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ try {
+ BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options);
+ return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase()));
+ } catch (FileNotFoundException e) {
+ return false;
}
- return path;
+ }
+
+ public String getOriginalPath(Uri uri) {
+ Log.d(Config.LOGTAG,"get original path for uri: "+uri.toString());
+ return FileUtils.getPath(mXmppConnectionService,uri);
}
public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
@@ -184,8 +185,18 @@ public class FileBackend {
return this.copyImageToPrivateStorage(message, image, 0);
}
- private DownloadableFile copyImageToPrivateStorage(Message message,
- Uri image, int sampleSize) throws FileCopyException {
+ private DownloadableFile copyImageToPrivateStorage(Message message,Uri image, int sampleSize) throws FileCopyException {
+ switch(Config.IMAGE_FORMAT) {
+ case JPEG:
+ message.setRelativeFilePath(message.getUuid()+".jpg");
+ break;
+ case PNG:
+ message.setRelativeFilePath(message.getUuid()+".png");
+ break;
+ case WEBP:
+ message.setRelativeFilePath(message.getUuid()+".webp");
+ break;
+ }
DownloadableFile file = getFile(message);
file.getParentFile().mkdirs();
InputStream is = null;
@@ -205,13 +216,13 @@ public class FileBackend {
if (originalBitmap == null) {
throw new FileCopyException(R.string.error_not_an_image_file);
}
- Bitmap scaledBitmap = resize(originalBitmap, IMAGE_SIZE);
+ Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
int rotation = getRotation(image);
if (rotation > 0) {
scaledBitmap = rotate(scaledBitmap, rotation);
}
- boolean success = scaledBitmap.compress(Bitmap.CompressFormat.WEBP, 75, os);
+ boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, Config.IMAGE_QUALITY, os);
if (!success) {
throw new FileCopyException(R.string.error_compressing_image);
}
@@ -346,11 +357,7 @@ public class FileBackend {
file.delete();
return false;
}
- } catch (FileNotFoundException e) {
- return false;
- } catch (IOException e) {
- return false;
- } catch (NoSuchAlgorithmException e) {
+ } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
return false;
} finally {
close(os);
@@ -377,6 +384,9 @@ public class FileBackend {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(image, size);
is = mXmppConnectionService.getContentResolver().openInputStream(image);
+ if (is == null) {
+ return null;
+ }
Bitmap input = BitmapFactory.decodeStream(is, null, options);
if (input == null) {
return null;
@@ -403,6 +413,9 @@ public class FileBackend {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(image,Math.max(newHeight, newWidth));
is = mXmppConnectionService.getContentResolver().openInputStream(image);
+ if (is == null) {
+ return null;
+ }
Bitmap source = BitmapFactory.decodeStream(is, null, options);
if (source == null) {
return null;
@@ -546,4 +559,13 @@ public class FileBackend {
}
}
}
+
+ public static void close(Socket socket) {
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ }
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
index 676a09c9..5def05dd 100644
--- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
@@ -1,5 +1,35 @@
package eu.siacs.conversations.services;
+import android.content.Context;
+import android.os.PowerManager;
+import android.util.Log;
+import android.util.Pair;
+
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.modes.AEADBlockCipher;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.DownloadableFile;
+
public class AbstractConnectionManager {
protected XmppConnectionService mXmppConnectionService;
@@ -20,4 +50,75 @@ public class AbstractConnectionManager {
return 524288;
}
}
+
+ public static Pair<InputStream,Integer> createInputStream(DownloadableFile file, boolean gcm) throws FileNotFoundException {
+ FileInputStream is;
+ int size;
+ is = new FileInputStream(file);
+ size = (int) file.getSize();
+ if (file.getKey() == null) {
+ return new Pair<InputStream,Integer>(is,size);
+ }
+ try {
+ if (gcm) {
+ AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
+ InputStream cis = new org.bouncycastle.crypto.io.CipherInputStream(is, cipher);
+ return new Pair<>(cis, cipher.getOutputSize(size));
+ } else {
+ IvParameterSpec ips = new IvParameterSpec(file.getIv());
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips);
+ Log.d(Config.LOGTAG, "opening encrypted input stream");
+ final int s = Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE ? size : (size / 16 + 1) * 16;
+ return new Pair<InputStream,Integer>(new CipherInputStream(is, cipher),s);
+ }
+ } catch (InvalidKeyException e) {
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (NoSuchPaddingException e) {
+ return null;
+ } catch (InvalidAlgorithmParameterException e) {
+ return null;
+ }
+ }
+
+ public static OutputStream createOutputStream(DownloadableFile file, boolean gcm) {
+ FileOutputStream os;
+ try {
+ os = new FileOutputStream(file);
+ if (file.getKey() == null) {
+ return os;
+ }
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ try {
+ if (gcm) {
+ AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
+ return new org.bouncycastle.crypto.io.CipherOutputStream(os, cipher);
+ } else {
+ IvParameterSpec ips = new IvParameterSpec(file.getIv());
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips);
+ Log.d(Config.LOGTAG, "opening encrypted output stream");
+ return new CipherOutputStream(os, cipher);
+ }
+ } catch (InvalidKeyException e) {
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (NoSuchPaddingException e) {
+ return null;
+ } catch (InvalidAlgorithmParameterException e) {
+ return null;
+ }
+ }
+
+ public PowerManager.WakeLock createWakeLock(String name) {
+ PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
+ return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name);
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
index dfbe9db7..ceab1592 100644
--- a/src/main/java/eu/siacs/conversations/services/EventReceiver.java
+++ b/src/main/java/eu/siacs/conversations/services/EventReceiver.java
@@ -1,10 +1,11 @@
package eu.siacs.conversations.services;
-import eu.siacs.conversations.persistance.DatabaseBackend;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+
public class EventReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
index 0bc428c8..a31848a7 100644
--- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
+++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
@@ -111,7 +111,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.ERROR) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString());
finalizeQuery(query);
}
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index 683fcb04..d03dcb84 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -65,7 +65,7 @@ public class NotificationService {
return (message.getStatus() == Message.STATUS_RECEIVED)
&& notificationsEnabled()
&& !message.getConversation().isMuted()
- && (message.getConversation().getMode() == Conversation.MODE_SINGLE
+ && (message.getConversation().isPnNA()
|| conferenceNotificationsEnabled()
|| wasHighlightedOrPrivate(message)
);
@@ -333,9 +333,10 @@ public class NotificationService {
private Message getImage(final Iterable<Message> messages) {
for (final Message message : messages) {
- if (message.getType() == Message.TYPE_IMAGE
+ if (message.getType() != Message.TYPE_TEXT
&& message.getTransferable() == null
- && message.getEncryption() != Message.ENCRYPTION_PGP) {
+ && message.getEncryption() != Message.ENCRYPTION_PGP
+ && message.getFileParams().height > 0) {
return message;
}
}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index f50b9fa6..b165ede4 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -53,16 +53,17 @@ import de.tzur.conversations.Settings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
-import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
@@ -86,6 +87,7 @@ import eu.siacs.conversations.xmpp.OnContactStatusChanged;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
import eu.siacs.conversations.xmpp.OnStatusChanged;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
@@ -213,7 +215,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.ERROR) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
Element error = packet.findChild("error");
String text = error != null ? error.findChildContent("text") : null;
if (text != null) {
@@ -274,11 +276,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
syncDirtyContacts(account);
- scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode());
+ account.getAxolotlService().publishOwnDeviceIdIfNeeded();
+ account.getAxolotlService().publishBundlesIfNeeded();
+
+ scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
} else if (account.getStatus() == Account.State.OFFLINE) {
resetSendingToWaiting(account);
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
- int timeToReconnect = mRandom.nextInt(50) + 10;
+ int timeToReconnect = mRandom.nextInt(20) + 10;
scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode());
}
} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
@@ -305,6 +310,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private int rosterChangedListenerCount = 0;
private OnMucRosterUpdate mOnMucRosterUpdate = null;
private int mucRosterChangedListenerCount = 0;
+ private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
+ private int keyStatusUpdatedListenerCount = 0;
private SecureRandom mRandom;
private OpenPgpServiceConnection pgpServiceConnection;
private PgpEngine mPgpEngine = null;
@@ -343,7 +350,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void attachLocationToConversation(final Conversation conversation,
final Uri uri,
final UiCallback<Message> callback) {
- int encryption = conversation.getNextEncryption(forceEncryption());
+ int encryption = conversation.getNextEncryption();
if (encryption == Message.ENCRYPTION_PGP) {
encryption = Message.ENCRYPTION_DECRYPTED;
}
@@ -362,12 +369,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
final Uri uri,
final UiCallback<Message> callback) {
final Message message;
- if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
- message = new Message(conversation, "",
- Message.ENCRYPTION_DECRYPTED);
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
+ message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
} else {
- message = new Message(conversation, "",
- conversation.getNextEncryption(forceEncryption()));
+ message = new Message(conversation, "", conversation.getNextEncryption());
}
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_FILE);
@@ -382,33 +387,35 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
} else {
mFileAddingExecutor.execute(new Runnable() {
- @Override
- public void run() {
- try {
- getFileBackend().copyFileToPrivateStorage(message, uri);
- getFileBackend().updateFileParams(message);
- if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
- getPgpEngine().encrypt(message, callback);
- } else {
- callback.success(message);
- }
- } catch (FileBackend.FileCopyException e) {
- callback.error(e.getResId(), message);
- }
- }
- });
+ @Override
+ public void run() {
+ try {
+ getFileBackend().copyFileToPrivateStorage(message, uri);
+ getFileBackend().updateFileParams(message);
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ getPgpEngine().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } catch (FileBackend.FileCopyException e) {
+ callback.error(e.getResId(), message);
+ }
+ }
+ });
}
}
- public void attachImageToConversation(final Conversation conversation,
- final Uri uri, final UiCallback<Message> callback) {
+ public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
+ if (getFileBackend().useImageAsIs(uri)) {
+ Log.d(Config.LOGTAG,"using image as is");
+ attachFileToConversation(conversation, uri, callback);
+ return;
+ }
final Message message;
- if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
- message = new Message(conversation, "",
- Message.ENCRYPTION_DECRYPTED);
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
+ message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
} else {
- message = new Message(conversation, "",
- conversation.getNextEncryption(forceEncryption()));
+ message = new Message(conversation, "",conversation.getNextEncryption());
}
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_IMAGE);
@@ -418,7 +425,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void run() {
try {
getFileBackend().copyImageToPrivateStorage(message, uri);
- if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
@@ -611,9 +618,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
this.accounts = databaseBackend.getAccounts();
- for (final Account account : this.accounts) {
- account.initAccountServices(this);
- }
restoreFromDatabase();
getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
@@ -694,22 +698,22 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
- private void sendFileMessage(final Message message) {
+ private void sendFileMessage(final Message message, final boolean delay) {
Log.d(Config.LOGTAG, "send file message");
final Account account = message.getConversation().getAccount();
final XmppConnection connection = account.getXmppConnection();
if (connection != null && connection.getFeatures().httpUpload()) {
- mHttpConnectionManager.createNewUploadConnection(message);
+ mHttpConnectionManager.createNewUploadConnection(message, delay);
} else {
mJingleConnectionManager.createNewConnection(message);
}
}
public void sendMessage(final Message message) {
- sendMessage(message, false);
+ sendMessage(message, false, false);
}
- private void sendMessage(final Message message, final boolean resend) {
+ private void sendMessage(final Message message, final boolean resend, final boolean delay) {
final Account account = message.getConversation().getAccount();
final Conversation conversation = message.getConversation();
account.deactivateGracePeriod();
@@ -719,7 +723,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
message.getConversation().endOtrIfNeeded();
- message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
+ message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
+ new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
markMessage(message,Message.STATUS_SEND_FAILED);
@@ -732,24 +737,24 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
case Message.ENCRYPTION_NONE:
if (message.needsUploading()) {
if (account.httpUploadAvailable() || message.fixCounterpart()) {
- this.sendFileMessage(message);
+ this.sendFileMessage(message,delay);
} else {
break;
}
} else {
- packet = mMessageGenerator.generateChat(message,resend);
+ packet = mMessageGenerator.generateChat(message);
}
break;
case Message.ENCRYPTION_PGP:
case Message.ENCRYPTION_DECRYPTED:
if (message.needsUploading()) {
if (account.httpUploadAvailable() || message.fixCounterpart()) {
- this.sendFileMessage(message);
+ this.sendFileMessage(message,delay);
} else {
break;
}
} else {
- packet = mMessageGenerator.generatePgpChat(message,resend);
+ packet = mMessageGenerator.generatePgpChat(message);
}
break;
case Message.ENCRYPTION_OTR:
@@ -763,7 +768,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (message.needsUploading()) {
mJingleConnectionManager.createNewConnection(message);
} else {
- packet = mMessageGenerator.generateOtrChat(message,resend);
+ packet = mMessageGenerator.generateOtrChat(message);
}
} else if (otrSession == null) {
if (message.fixCounterpart()) {
@@ -773,6 +778,24 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
break;
+ case Message.ENCRYPTION_AXOLOTL:
+ message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
+ if (message.needsUploading()) {
+ if (account.httpUploadAvailable() || message.fixCounterpart()) {
+ this.sendFileMessage(message,delay);
+ } else {
+ break;
+ }
+ } else {
+ XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
+ if (axolotlMessage == null) {
+ account.getAxolotlService().preparePayloadMessage(message, delay);
+ } else {
+ packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
+ }
+ }
+ break;
+
}
if (packet != null) {
if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
@@ -800,6 +823,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
}
break;
+ case Message.ENCRYPTION_AXOLOTL:
+ message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
+ break;
}
}
@@ -819,6 +845,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
updateConversationUi();
}
if (packet != null) {
+ if (delay) {
+ mMessageGenerator.addDelay(packet,message.getTimeSent());
+ }
if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
if (this.sendChatStates()) {
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
@@ -833,13 +862,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
@Override
public void onMessageFound(Message message) {
- resendMessage(message);
+ resendMessage(message, true);
}
});
}
- public void resendMessage(final Message message) {
- sendMessage(message, true);
+ public void resendMessage(final Message message, final boolean delay) {
+ sendMessage(message, true, delay);
}
public void fetchRosterFromServer(final Account account) {
@@ -850,8 +879,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} else {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
}
- iqPacket.query(Xmlns.ROSTER).setAttribute("ver",account.getRosterVersion());
- sendIqPacket(account, iqPacket, mIqParser);
+ iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion());
+ sendIqPacket(account,iqPacket,mIqParser);
}
public void fetchBookmarks(final Account account) {
@@ -862,28 +891,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
- final Element query = packet.query();
- final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
- final Element storage = query.findChild("storage",
- "storage:bookmarks");
- if (storage != null) {
- for (final Element item : storage.getChildren()) {
- if (item.getName().equals("conference")) {
- final Bookmark bookmark = Bookmark.parse(item, account);
- bookmarks.add(bookmark);
- Conversation conversation = find(bookmark);
- if (conversation != null) {
- conversation.setBookmark(bookmark);
- } else if (bookmark.autojoin() && bookmark.getJid() != null) {
- conversation = findOrCreateConversation(
- account, bookmark.getJid(), true);
- conversation.setBookmark(bookmark);
- joinMuc(conversation);
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ final Element query = packet.query();
+ final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
+ final Element storage = query.findChild("storage", "storage:bookmarks");
+ if (storage != null) {
+ for (final Element item : storage.getChildren()) {
+ if (item.getName().equals("conference")) {
+ final Bookmark bookmark = Bookmark.parse(item, account);
+ bookmarks.add(bookmark);
+ Conversation conversation = find(bookmark);
+ if (conversation != null) {
+ conversation.setBookmark(bookmark);
+ } else if (bookmark.autojoin() && bookmark.getJid() != null) {
+ conversation = findOrCreateConversation(
+ account, bookmark.getJid(), true);
+ conversation.setBookmark(bookmark);
+ joinMuc(conversation);
+ }
}
}
}
+ account.setBookmarks(bookmarks);
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fetch bookmarks");
}
- account.setBookmarks(bookmarks);
}
};
sendIqPacket(account, iqPacket, callback);
@@ -963,6 +995,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
Log.d(Config.LOGTAG,"restoring roster");
for(Account account : accounts) {
databaseBackend.readRoster(account.getRoster());
+ account.initAccountServices(XmppConnectionService.this);
}
getBitmapCache().evictAll();
Looper.prepare();
@@ -994,6 +1027,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void onMessageFound(Message message) {
if (!getFileBackend().isFileAvailable(message)) {
message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
+ final int s = message.getStatus();
+ if(s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
+ markMessage(message,Message.STATUS_SEND_FAILED);
+ }
}
}
});
@@ -1005,7 +1042,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (message != null) {
if (!getFileBackend().isFileAvailable(message)) {
message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
- updateConversationUi();
+ final int s = message.getStatus();
+ if(s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
+ markMessage(message,Message.STATUS_SEND_FAILED);
+ } else {
+ updateConversationUi();
+ }
}
return;
}
@@ -1029,19 +1071,19 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
Collections.sort(list, new Comparator<Conversation>() {
- @Override
- public int compare(Conversation lhs, Conversation rhs) {
- Message left = lhs.getLatestMessage();
- Message right = rhs.getLatestMessage();
- if (left.getTimeSent() > right.getTimeSent()) {
- return -1;
- } else if (left.getTimeSent() < right.getTimeSent()) {
- return 1;
- } else {
- return 0;
- }
- }
- });
+ @Override
+ public int compare(Conversation lhs, Conversation rhs) {
+ Message left = lhs.getLatestMessage();
+ Message right = rhs.getLatestMessage();
+ if (left.getTimeSent() > right.getTimeSent()) {
+ return -1;
+ } else if (left.getTimeSent() < right.getTimeSent()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ });
}
public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
@@ -1370,6 +1412,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
+ public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
+ synchronized (this) {
+ if (checkListeners()) {
+ switchToForeground();
+ }
+ this.mOnKeyStatusUpdated = listener;
+ if (this.keyStatusUpdatedListenerCount < 2) {
+ this.keyStatusUpdatedListenerCount++;
+ }
+ }
+ }
+
+ public void removeOnNewKeysAvailableListener() {
+ synchronized (this) {
+ this.keyStatusUpdatedListenerCount--;
+ if (this.keyStatusUpdatedListenerCount <= 0) {
+ this.keyStatusUpdatedListenerCount = 0;
+ this.mOnKeyStatusUpdated = null;
+ if (checkListeners()) {
+ switchToBackground();
+ }
+ }
+ }
+ }
+
public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
synchronized (this) {
if (checkListeners()) {
@@ -1400,7 +1467,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
&& this.mOnConversationUpdate == null
&& this.mOnRosterUpdate == null
&& this.mOnUpdateBlocklist == null
- && this.mOnShowErrorToast == null);
+ && this.mOnShowErrorToast == null
+ && this.mOnKeyStatusUpdated == null);
}
private void switchToForeground() {
@@ -1638,7 +1706,7 @@ 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.ERROR) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
ArrayList<String> features = new ArrayList<>();
for (Element child : packet.query().getChildren()) {
if (child != null && child.getName().equals("feature")) {
@@ -1662,7 +1730,7 @@ 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.ERROR) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
for (Field field : data.getFields()) {
if (options.containsKey(field.getName())) {
@@ -1676,12 +1744,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
sendIqPacket(account, set, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.RESULT) {
- if (callback != null) {
+ if (callback != null) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
callback.onPushSucceeded();
- }
- } else {
- if (callback != null) {
+ } else {
callback.onPushFailed();
}
}
@@ -1812,7 +1878,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
account.getJid().toBareJid() + " otr session established with "
+ conversation.getJid() + "/"
+ otrSession.getSessionID().getUserID());
- conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
+ conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
@@ -1825,8 +1891,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (message.needsUploading()) {
mJingleConnectionManager.createNewConnection(message);
} else {
- MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true);
+ MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
if (outPacket != null) {
+ mMessageGenerator.addDelay(outPacket, message.getTimeSent());
message.setStatus(Message.STATUS_SEND);
databaseBackend.updateMessage(message);
sendMessagePacket(account, outPacket);
@@ -1917,10 +1984,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
final IqPacket packet = XmppConnectionService.this.mIqGenerator
.publishAvatarMetadata(avatar);
sendIqPacket(account, packet, new OnIqPacketReceived() {
-
@Override
- public void onIqPacketReceived(Account account,
- IqPacket result) {
+ public void onIqPacketReceived(Account account, IqPacket result) {
if (result.getType() == IqPacket.TYPE.RESULT) {
if (account.setAvatar(avatar.getFilename())) {
getAvatarService().clear(account);
@@ -2226,6 +2291,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public boolean forceEncryption() {
return getPreferences().getBoolean("force_encryption", false);
}
+
public boolean confirmMessages() {
return getPreferences().getBoolean("confirm_messages", true);
}
@@ -2287,6 +2353,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
+ public void keyStatusUpdated() {
+ if(mOnKeyStatusUpdated != null) {
+ mOnKeyStatusUpdated.onKeyStatusUpdated();
+ }
+ }
+
public Account findAccountByJid(final Jid accountJid) {
for (Account account : this.accounts) {
if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
@@ -2497,8 +2569,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
for (final Message msg : messages) {
+ msg.setTime(System.currentTimeMillis());
markMessage(msg, Message.STATUS_WAITING);
- this.resendMessage(msg);
+ this.resendMessage(msg,false);
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java
index a57e1b89..bd2042fb 100644
--- a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java
+++ b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java
@@ -2,7 +2,6 @@ package eu.siacs.conversations.ui;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
import android.preference.Preference;
import android.util.AttributeSet;
diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java
index 13d7f4fc..08594201 100644
--- a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java
@@ -55,16 +55,10 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
}
Collections.sort(getListItems());
}
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- getListItemAdapter().notifyDataSetChanged();
- }
- });
+ getListItemAdapter().notifyDataSetChanged();
}
- @Override
- public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) {
+ protected void refreshUiReal() {
final Editable editable = getSearchEditText().getText();
if (editable != null) {
filterContacts(editable.toString());
@@ -72,4 +66,9 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
filterContacts();
}
}
+
+ @Override
+ public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) {
+ refreshUi();
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java
index aac435fd..09988b7c 100644
--- a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java
@@ -4,7 +4,6 @@ import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
-import android.widget.TextView;
import android.widget.Toast;
import eu.siacs.conversations.R;
@@ -107,4 +106,8 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
});
}
+
+ public void refreshUiReal() {
+
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
index c9e99ce5..bc23c3ba 100644
--- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
@@ -13,11 +13,11 @@ import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.ListView;
-import java.util.Set;
-import java.util.HashSet;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
-import java.util.ArrayList;
+import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@@ -149,4 +149,8 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
return result.toArray(new String[result.size()]);
}
+
+ public void refreshUiReal() {
+ //nothing to do. This Activity doesn't implement any listeners
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
index 07b8819d..12b66c35 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
@@ -27,8 +27,8 @@ import org.openintents.openpgp.util.OpenPgpUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
-import java.util.List;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.entities.Account;
@@ -38,8 +38,8 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.User;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
+import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
import eu.siacs.conversations.xmpp.jid.Jid;
public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed {
@@ -266,14 +266,17 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
final User self = mConversation.getMucOptions().getSelf();
this.mSelectedUser = user;
String name;
+ final Contact contact = user.getContact();
+ if (contact != null) {
+ name = contact.getDisplayName();
+ } else if (user.getJid() != null){
+ name = user.getJid().toBareJid().toString();
+ } else {
+ name = user.getName();
+ }
+ menu.setHeaderTitle(name);
if (user.getJid() != null) {
- final Contact contact = user.getContact();
- if (contact != null) {
- name = contact.getDisplayName();
- } else {
- name = user.getJid().toBareJid().toString();
- }
- menu.setHeaderTitle(name);
+ MenuItem showContactDetails = menu.findItem(R.id.action_contact_details);
MenuItem startConversation = menu.findItem(R.id.start_conversation);
MenuItem giveMembership = menu.findItem(R.id.give_membership);
MenuItem removeMembership = menu.findItem(R.id.remove_membership);
@@ -282,6 +285,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room);
MenuItem banFromConference = menu.findItem(R.id.ban_from_conference);
startConversation.setVisible(true);
+ if (contact != null) {
+ showContactDetails.setVisible(true);
+ }
if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) &&
self.getAffiliation().outranks(user.getAffiliation())) {
if (mAdvancedMode) {
@@ -300,15 +306,24 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
removeAdminPrivileges.setVisible(true);
}
}
+ } else {
+ MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message);
+ sendPrivateMessage.setVisible(true);
}
}
- super.onCreateContextMenu(menu,v,menuInfo);
+ super.onCreateContextMenu(menu, v, menuInfo);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
+ case R.id.action_contact_details:
+ Contact contact = mSelectedUser.getContact();
+ if (contact != null) {
+ switchToContactDetails(contact);
+ }
+ return true;
case R.id.start_conversation:
startConversation(mSelectedUser);
return true;
@@ -331,6 +346,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.OUTCAST,this);
xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,this);
return true;
+ case R.id.send_private_message:
+ privateMsgInMuc(mConversation,mSelectedUser.getName());
+ return true;
default:
return super.onContextItemSelected(item);
}
@@ -404,8 +422,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
private void updateView() {
final MucOptions mucOptions = mConversation.getMucOptions();
final User self = mucOptions.getSelf();
- mAccountJid.setText(getString(R.string.using_account, mConversation
- .getAccount().getJid().toBareJid()));
+ String account;
+ if (Config.DOMAIN_LOCK != null) {
+ account = mConversation.getAccount().getJid().getLocalpart();
+ } else {
+ account = mConversation.getAccount().getJid().toBareJid().toString();
+ }
+ 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());
diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
index c190caed..d98e9164 100644
--- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -29,9 +29,11 @@ import android.widget.QuickContactBadge;
import android.widget.TextView;
import org.openintents.openpgp.util.OpenPgpUtils;
+import org.whispersystems.libaxolotl.IdentityKey;
import java.util.List;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.entities.Account;
@@ -41,12 +43,13 @@ import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
-public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist {
+public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
public static final String ACTION_VIEW_CONTACT = "view_contact";
private Contact contact;
@@ -108,6 +111,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
private LinearLayout keys;
private LinearLayout tags;
private boolean showDynamicTags;
+ private String messageFingerprint;
private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
@@ -158,6 +162,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
@Override
+ public void OnUpdateBlocklist(final Status status) {
+ refreshUi();
+ }
+
+ @Override
protected void refreshUiReal() {
invalidateOptionsMenu();
populateView();
@@ -185,6 +194,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
} catch (final InvalidJidException ignored) {
}
}
+ this.messageFingerprint = getIntent().getStringExtra("fingerprint");
setContentView(R.layout.activity_contact_details);
contactJidTv = (TextView) findViewById(R.id.details_contactjid);
@@ -350,7 +360,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
} else {
contactJidTv.setText(contact.getJid().toString());
}
- accountJidTv.setText(getString(R.string.using_account, contact.getAccount().getJid().toBareJid()));
+ String account;
+ if (Config.DOMAIN_LOCK != null) {
+ account = contact.getAccount().getJid().getLocalpart();
+ } else {
+ account = contact.getAccount().getJid().toBareJid().toString();
+ }
+ accountJidTv.setText(getString(R.string.using_account, account));
badge.setImageBitmap(avatarService().get(contact, getPixel(72)));
badge.setOnClickListener(this.onBadgeClick);
@@ -362,13 +378,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
View view = inflater.inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
- ImageButton remove = (ImageButton) view
+ ImageButton removeButton = (ImageButton) view
.findViewById(R.id.button_remove);
- remove.setVisibility(View.VISIBLE);
+ removeButton.setVisibility(View.VISIBLE);
keyType.setText("OTR Fingerprint");
key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
keys.addView(view);
- remove.setOnClickListener(new OnClickListener() {
+ removeButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
@@ -376,6 +392,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
});
}
+ for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
+ contact.getAccount(), contact.getJid().toBareJid().toString())) {
+ boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint);
+ hasKeys |= addFingerprintRow(keys, contact.getAccount(), identityKey, highlight);
+ }
if (contact.getPgpKeyId() != 0) {
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, keys, false);
@@ -460,14 +481,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
@Override
- public void OnUpdateBlocklist(final Status status) {
- runOnUiThread(new Runnable() {
-
- @Override
- public void run() {
- invalidateOptionsMenu();
- populateView();
- }
- });
+ public void onKeyStatusUpdated() {
+ refreshUi();
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
index bf261dba..7fec827b 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
@@ -16,6 +16,7 @@ import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.widget.SlidingPaneLayout;
import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
+import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -28,13 +29,16 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.Toast;
import net.java.otr4j.session.SessionStatus;
-import de.timroes.android.listview.EnhancedListView;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import de.timroes.android.listview.EnhancedListView;
+import eu.siacs.conversations.Config;
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.Blockable;
import eu.siacs.conversations.entities.Contact;
@@ -47,6 +51,8 @@ import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.ui.adapter.ConversationAdapter;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
import github.ankushsachdeva.emojicon.EmojiconEditText;
public class ConversationActivity extends XmppActivity
@@ -59,15 +65,19 @@ public class ConversationActivity extends XmppActivity
public static final String MESSAGE = "messageUuid";
public static final String TEXT = "text";
public static final String NICK = "nick";
+ public static final String PRIVATE_MESSAGE = "pm";
public static final int REQUEST_SEND_MESSAGE = 0x0201;
public static final int REQUEST_DECRYPT_PGP = 0x0202;
public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
+ public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208;
+ public static final int REQUEST_TRUST_KEYS_MENU = 0x0209;
public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304;
public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
+ public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
private static final String STATE_PANEL_OPEN = "state_panel_open";
private static final String STATE_PENDING_URI = "state_pending_uri";
@@ -77,6 +87,7 @@ public class ConversationActivity extends XmppActivity
final private List<Uri> mPendingImageUris = new ArrayList<>();
final private List<Uri> mPendingFileUris = new ArrayList<>();
private Uri mPendingGeoUri = null;
+ private boolean forbidProcessingPendings = false;
private View mContentView;
@@ -188,64 +199,64 @@ public class ConversationActivity extends XmppActivity
listView.setDismissCallback(new EnhancedListView.OnDismissCallback() {
- @Override
- public EnhancedListView.Undoable onDismiss(final EnhancedListView enhancedListView, final int position) {
-
- final int index = listView.getFirstVisiblePosition();
- View v = listView.getChildAt(0);
- final int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop());
-
- swipedConversation = listAdapter.getItem(position);
- listAdapter.remove(swipedConversation);
- swipedConversation.markRead();
- xmppConnectionService.getNotificationService().clear(swipedConversation);
-
- final boolean formerlySelected = (getSelectedConversation() == swipedConversation);
- if (position == 0 && listAdapter.getCount() == 0) {
- endConversation(swipedConversation, false, true);
- return null;
- } else if (formerlySelected) {
- setSelectedConversation(listAdapter.getItem(0));
- ConversationActivity.this.mConversationFragment
- .reInit(getSelectedConversation());
- }
-
- return new EnhancedListView.Undoable() {
-
- @Override
- public void undo() {
- listAdapter.insert(swipedConversation, position);
- if (formerlySelected) {
- setSelectedConversation(swipedConversation);
- ConversationActivity.this.mConversationFragment
- .reInit(getSelectedConversation());
- }
- swipedConversation = null;
- listView.setSelectionFromTop(index + (listView.getChildCount() < position ? 1 : 0), top);
- }
-
- @Override
- public void discard() {
- if (!swipedConversation.isRead()
- && swipedConversation.getMode() == Conversation.MODE_SINGLE) {
- swipedConversation = null;
- return;
- }
- endConversation(swipedConversation, false, false);
- swipedConversation = null;
- }
-
- @Override
- public String getTitle() {
- if (swipedConversation.getMode() == Conversation.MODE_MULTI) {
- return getResources().getString(R.string.title_undo_swipe_out_muc);
- } else {
- return getResources().getString(R.string.title_undo_swipe_out_conversation);
- }
- }
- };
- }
- });
+ @Override
+ public EnhancedListView.Undoable onDismiss(final EnhancedListView enhancedListView, final int position) {
+
+ final int index = listView.getFirstVisiblePosition();
+ View v = listView.getChildAt(0);
+ final int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop());
+
+ swipedConversation = listAdapter.getItem(position);
+ listAdapter.remove(swipedConversation);
+ swipedConversation.markRead();
+ xmppConnectionService.getNotificationService().clear(swipedConversation);
+
+ final boolean formerlySelected = (getSelectedConversation() == swipedConversation);
+ if (position == 0 && listAdapter.getCount() == 0) {
+ endConversation(swipedConversation, false, true);
+ return null;
+ } else if (formerlySelected) {
+ setSelectedConversation(listAdapter.getItem(0));
+ ConversationActivity.this.mConversationFragment
+ .reInit(getSelectedConversation());
+ }
+
+ return new EnhancedListView.Undoable() {
+
+ @Override
+ public void undo() {
+ listAdapter.insert(swipedConversation, position);
+ if (formerlySelected) {
+ setSelectedConversation(swipedConversation);
+ ConversationActivity.this.mConversationFragment
+ .reInit(getSelectedConversation());
+ }
+ swipedConversation = null;
+ listView.setSelectionFromTop(index + (listView.getChildCount() < position ? 1 : 0), top);
+ }
+
+ @Override
+ public void discard() {
+ if (!swipedConversation.isRead()
+ && swipedConversation.getMode() == Conversation.MODE_SINGLE) {
+ swipedConversation = null;
+ return;
+ }
+ endConversation(swipedConversation, false, false);
+ swipedConversation = null;
+ }
+
+ @Override
+ public String getTitle() {
+ if (swipedConversation.getMode() == Conversation.MODE_MULTI) {
+ return getResources().getString(R.string.title_undo_swipe_out_muc);
+ } else {
+ return getResources().getString(R.string.title_undo_swipe_out_conversation);
+ }
+ }
+ };
+ }
+ });
listView.enableSwipeToDismiss();
listView.setSwipeDirection(EnhancedListView.SwipeDirection.START);
listView.setSwipingLayout(R.id.swipeable_item);
@@ -376,7 +387,7 @@ public class ConversationActivity extends XmppActivity
} else {
menuAdd.setVisible(!isConversationsOverviewHideable());
if (this.getSelectedConversation() != null) {
- if (this.getSelectedConversation().getNextEncryption(forceEncryption()) != Message.ENCRYPTION_NONE) {
+ if (this.getSelectedConversation().getNextEncryption() != Message.ENCRYPTION_NONE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
} else {
@@ -387,6 +398,7 @@ public class ConversationActivity extends XmppActivity
menuContactDetails.setVisible(false);
menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable());
menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
+ menuSecure.setVisible(!Config.HIDE_PGP_IN_UI); //if pgp is hidden conferences have no choice of encryption
} else {
menuMucDetails.setVisible(false);
}
@@ -400,7 +412,7 @@ public class ConversationActivity extends XmppActivity
return true;
}
- private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
+ protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
final Conversation conversation = getSelectedConversation();
final Account account = conversation.getAccount();
final OnPresenceSelected callback = new OnPresenceSelected() {
@@ -489,7 +501,7 @@ public class ConversationActivity extends XmppActivity
break;
}
final Conversation conversation = getSelectedConversation();
- final int encryption = conversation.getNextEncryption(forceEncryption());
+ final int encryption = conversation.getNextEncryption();
if (encryption == Message.ENCRYPTION_PGP) {
if (hasPgp()) {
if (conversation.getContact().getPgpKeyId() != 0) {
@@ -536,7 +548,9 @@ public class ConversationActivity extends XmppActivity
showInstallPgpDialog();
}
} else {
- selectPresenceToAttachFile(attachmentChoice, encryption);
+ if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) {
+ selectPresenceToAttachFile(attachmentChoice, encryption);
+ }
}
}
@@ -701,13 +715,13 @@ public class ConversationActivity extends XmppActivity
intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString());
switch (menuItem.getItemId()) {
case R.id.scan_fingerprint:
- intent.putExtra("mode", VerifyOTRActivity.MODE_SCAN_FINGERPRINT);
+ intent.putExtra("mode",VerifyOTRActivity.MODE_SCAN_FINGERPRINT);
break;
case R.id.ask_question:
- intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION);
+ intent.putExtra("mode",VerifyOTRActivity.MODE_ASK_QUESTION);
break;
case R.id.manual_verification:
- intent.putExtra("mode", VerifyOTRActivity.MODE_MANUAL_VERIFICATION);
+ intent.putExtra("mode",VerifyOTRActivity.MODE_MANUAL_VERIFICATION);
break;
}
startActivity(intent);
@@ -751,6 +765,12 @@ public class ConversationActivity extends XmppActivity
showInstallPgpDialog();
}
break;
+ case R.id.encryption_choice_axolotl:
+ Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount())
+ + "Enabled axolotl for Contact " + conversation.getContact().getJid());
+ conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
+ item.setChecked(true);
+ break;
default:
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
break;
@@ -758,6 +778,7 @@ public class ConversationActivity extends XmppActivity
xmppConnectionService.databaseBackend.updateConversation(conversation);
fragment.updateChatMsgHint();
invalidateOptionsMenu();
+ refreshUi();
return true;
}
});
@@ -765,14 +786,15 @@ public class ConversationActivity extends XmppActivity
MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
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);
if (conversation.getMode() == Conversation.MODE_MULTI) {
- otr.setEnabled(false);
- } else {
- if (forceEncryption()) {
- none.setVisible(false);
- }
+ otr.setVisible(false);
+ axolotl.setVisible(false);
+ } else if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
+ axolotl.setEnabled(false);
}
- switch (conversation.getNextEncryption(forceEncryption())) {
+ switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_NONE:
none.setChecked(true);
break;
@@ -782,6 +804,9 @@ public class ConversationActivity extends XmppActivity
case Message.ENCRYPTION_PGP:
pgp.setChecked(true);
break;
+ case Message.ENCRYPTION_AXOLOTL:
+ axolotl.setChecked(true);
+ break;
default:
none.setChecked(true);
break;
@@ -793,8 +818,7 @@ public class ConversationActivity extends XmppActivity
protected void muteConversationDialog(final Conversation conversation) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.disable_notifications);
- final int[] durations = getResources().getIntArray(
- R.array.mute_options_durations);
+ final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
builder.setItems(R.array.mute_options_descriptions,
new OnClickListener() {
@@ -808,7 +832,7 @@ public class ConversationActivity extends XmppActivity
}
conversation.setMutedTill(till);
ConversationActivity.this.xmppConnectionService.databaseBackend
- .updateConversation(conversation);
+ .updateConversation(conversation);
updateConversationList();
ConversationActivity.this.mConversationFragment.updateMessages();
invalidateOptionsMenu();
@@ -946,18 +970,23 @@ public class ConversationActivity extends XmppActivity
this.mConversationFragment.reInit(getSelectedConversation());
}
- for(Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
- attachImageToConversation(getSelectedConversation(),i.next());
- }
+ if(!forbidProcessingPendings) {
+ for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
+ Uri foo = i.next();
+ attachImageToConversation(getSelectedConversation(), foo);
+ }
- for(Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
- attachFileToConversation(getSelectedConversation(),i.next());
- }
+ for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
+ attachFileToConversation(getSelectedConversation(), i.next());
+ }
- if (mPendingGeoUri != null) {
- attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
- mPendingGeoUri = null;
+ if (mPendingGeoUri != null) {
+ attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
+ mPendingGeoUri = null;
+ }
}
+ forbidProcessingPendings = false;
+
ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
setIntent(new Intent());
}
@@ -967,10 +996,21 @@ public class ConversationActivity extends XmppActivity
final String downloadUuid = intent.getStringExtra(MESSAGE);
final String text = intent.getStringExtra(TEXT);
final String nick = intent.getStringExtra(NICK);
+ final boolean pm = intent.getBooleanExtra(PRIVATE_MESSAGE,false);
if (selectConversationByUuid(uuid)) {
this.mConversationFragment.reInit(getSelectedConversation());
if (nick != null) {
- this.mConversationFragment.highlightInConference(nick);
+ if (pm) {
+ Jid jid = getSelectedConversation().getJid();
+ try {
+ Jid next = Jid.fromParts(jid.getLocalpart(),jid.getDomainpart(),nick);
+ this.mConversationFragment.privateMessageWith(next);
+ } catch (final InvalidJidException ignored) {
+ //do nothing
+ }
+ } else {
+ this.mConversationFragment.highlightInConference(nick);
+ }
} else {
this.mConversationFragment.appendText(text);
}
@@ -1067,6 +1107,9 @@ public class ConversationActivity extends XmppActivity
attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
this.mPendingGeoUri = null;
}
+ } else if (requestCode == REQUEST_TRUST_KEYS_TEXT || requestCode == REQUEST_TRUST_KEYS_MENU) {
+ this.forbidProcessingPendings = !xmppConnectionServiceBound;
+ mConversationFragment.onActivityResult(requestCode, resultCode, data);
}
} else {
mPendingImageUris.clear();
@@ -1207,10 +1250,6 @@ public class ConversationActivity extends XmppActivity
});
}
- public boolean forceEncryption() {
- return getPreferences().getBoolean("force_encryption", false);
- }
-
public boolean useSendButtonToIndicateStatus() {
return getPreferences().getBoolean("send_button_status", false);
}
@@ -1219,6 +1258,30 @@ public class ConversationActivity extends XmppActivity
return getPreferences().getBoolean("indicate_received", false);
}
+ protected boolean trustKeysIfNeeded(int requestCode) {
+ return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID);
+ }
+
+ protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
+ AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
+ boolean hasPendingKeys = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED,
+ mSelectedConversation.getContact()).isEmpty()
+ || !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
+ boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0;
+ if( hasPendingKeys || hasNoTrustedKeys) {
+ axolotlService.createSessionsIfNeeded(mSelectedConversation);
+ Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
+ intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString());
+ intent.putExtra("account", mSelectedConversation.getAccount().getJid().toBareJid().toString());
+ intent.putExtra("choice", attachmentChoice);
+ intent.putExtra("has_no_trusted", hasNoTrustedKeys);
+ startActivityForResult(intent, requestCode);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
@Override
protected void refreshUiReal() {
updateConversationList();
@@ -1240,6 +1303,7 @@ public class ConversationActivity extends XmppActivity
ConversationActivity.this.mConversationFragment.updateMessages();
updateActionBarTitle();
}
+ invalidateOptionsMenu();
}
@Override
@@ -1260,12 +1324,6 @@ public class ConversationActivity extends XmppActivity
@Override
public void OnUpdateBlocklist(Status status) {
this.refreshUi();
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- invalidateOptionsMenu();
- }
- });
}
public void unblockConversation(final Blockable conversation) {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index eb2688fd..f645b241 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -1,5 +1,6 @@
package eu.siacs.conversations.ui;
+import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.PendingIntent;
@@ -48,12 +49,18 @@ import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
+<<<<<<< HEAD
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.TransferablePlaceholder;
+=======
+import eu.siacs.conversations.entities.DownloadableFile;
+>>>>>>> 1.6.3
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
@@ -117,6 +124,96 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private RelativeLayout snackbar;
private TextView snackbarMessage;
private TextView snackbarAction;
+ private boolean messagesLoaded = true;
+ private Toast messageLoaderToast;
+
+ private OnScrollListener mOnScrollListener = new OnScrollListener() {
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ // TODO Auto-generated method stub
+
+ }
+
+ private int getIndexOf(String uuid, List<Message> messages) {
+ if (uuid == null) {
+ return 0;
+ }
+ for(int i = 0; i < messages.size(); ++i) {
+ if (uuid.equals(messages.get(i).getUuid())) {
+ return i;
+ } else {
+ Message next = messages.get(i);
+ while(next != null && next.wasMergedIntoPrevious()) {
+ if (uuid.equals(next.getUuid())) {
+ return i;
+ }
+ next = next.next();
+ }
+
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem,
+ int visibleItemCount, int totalItemCount) {
+ synchronized (ConversationFragment.this.messageList) {
+ if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) {
+ long timestamp = ConversationFragment.this.messageList.get(0).getTimeSent();
+ messagesLoaded = false;
+ activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
+ @Override
+ public void onMoreMessagesLoaded(final int c, Conversation conversation) {
+ if (ConversationFragment.this.conversation != conversation) {
+ return;
+ }
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final int oldPosition = messagesView.getFirstVisiblePosition();
+ Message message = messageList.get(oldPosition);
+ String uuid = message != null ? message.getUuid() : null;
+ View v = messagesView.getChildAt(0);
+ final int pxOffset = (v == null) ? 0 : v.getTop();
+ ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList);
+ updateStatusMessages();
+ messageListAdapter.notifyDataSetChanged();
+ int pos = getIndexOf(uuid,messageList);
+ messagesView.setSelectionFromTop(pos, pxOffset);
+ messagesLoaded = true;
+ if (messageLoaderToast != null) {
+ messageLoaderToast.cancel();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void informUser(final int resId) {
+
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (messageLoaderToast != null) {
+ messageLoaderToast.cancel();
+ }
+ if (ConversationFragment.this.conversation != conversation) {
+ return;
+ }
+ messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG);
+ messageLoaderToast.show();
+ }
+ });
+
+ }
+ });
+
+ }
+ }
+ }
+ };
private IntentSender askForPassphraseIntent = null;
protected OnClickListener clickToDecryptListener = new OnClickListener() {
@@ -212,19 +309,27 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (body.length() == 0 || this.conversation == null) {
return;
}
- Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption()));
+ Message message = new Message(conversation, body, conversation.getNextEncryption());
if (conversation.getMode() == Conversation.MODE_MULTI) {
if (conversation.getNextCounterpart() != null) {
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_PRIVATE);
}
}
- if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) {
- sendOtrMessage(message);
- } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
- sendPgpMessage(message);
- } else {
- sendPlainTextMessage(message);
+ switch (conversation.getNextEncryption()) {
+ case Message.ENCRYPTION_OTR:
+ sendOtrMessage(message);
+ break;
+ case Message.ENCRYPTION_PGP:
+ sendPgpMessage(message);
+ break;
+ case Message.ENCRYPTION_AXOLOTL:
+ if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
+ sendAxolotlMessage(message);
+ }
+ break;
+ default:
+ sendPlainTextMessage(message);
}
}
@@ -232,10 +337,10 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (conversation.getMode() == Conversation.MODE_MULTI
&& conversation.getNextCounterpart() != null) {
this.mEditMessage.setHint(getString(
- R.string.send_private_message_to,
- conversation.getNextCounterpart().getResourcepart()));
+ R.string.send_private_message_to,
+ conversation.getNextCounterpart().getResourcepart()));
} else {
- switch (conversation.getNextEncryption(activity.forceEncryption())) {
+ switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_NONE:
mEditMessage
.setHint(getString(R.string.send_plain_text_message));
@@ -243,6 +348,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
case Message.ENCRYPTION_OTR:
mEditMessage.setHint(getString(R.string.send_otr_message));
break;
+ case Message.ENCRYPTION_AXOLOTL:
+ mEditMessage.setHint(getString(R.string.send_omemo_message));
+ break;
case Message.ENCRYPTION_PGP:
mEditMessage.setHint(getString(R.string.send_pgp_message));
break;
@@ -388,6 +496,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
snackbarAction = (TextView) view.findViewById(R.id.snackbar_action);
messagesView = (ListView) view.findViewById(R.id.messages_view);
+ messagesView.setOnScrollListener(mOnScrollListener);
messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList);
messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() {
@@ -397,19 +506,20 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (message.getStatus() <= Message.STATUS_RECEIVED) {
if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
if (message.getCounterpart() != null) {
- if (!message.getCounterpart().isBareJid()) {
- highlightInConference(message.getCounterpart().getResourcepart());
- } else {
- highlightInConference(message.getCounterpart().toString());
+ String user = message.getCounterpart().isBareJid() ? message.getCounterpart().toString() : message.getCounterpart().getResourcepart();
+ if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
+ Toast.makeText(activity,activity.getString(R.string.user_has_left_conference,user),Toast.LENGTH_SHORT).show();
}
+ highlightInConference(user);
}
} else {
- activity.switchToContactDetails(message.getContact());
+ activity.switchToContactDetails(message.getContact(), message.getAxolotlFingerprint());
}
} else {
Account account = message.getConversation().getAccount();
Intent intent = new Intent(activity, EditAccountActivity.class);
intent.putExtra("jid", account.getJid().toBareJid().toString());
+ intent.putExtra("fingerprint", message.getAxolotlFingerprint());
startActivity(intent);
}
}
@@ -422,7 +532,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (message.getStatus() <= Message.STATUS_RECEIVED) {
if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
if (message.getCounterpart() != null) {
- privateMessageWith(message.getCounterpart());
+ String user = message.getCounterpart().getResourcepart();
+ if (user != null) {
+ if (message.getConversation().getMucOptions().isUserInRoom(user)) {
+ privateMessageWith(message.getCounterpart());
+ } else {
+ Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user), Toast.LENGTH_SHORT).show();
+ }
+ }
}
}
} else {
@@ -589,7 +706,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private void downloadFile(Message message) {
activity.xmppConnectionService.getHttpConnectionManager()
- .createNewDownloadConnection(message);
+ .createNewDownloadConnection(message,true);
}
private void cancelTransmission(Message message) {
@@ -670,6 +787,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
this.mEditMessage.setKeyboardListener(this);
this.messagesView.setAdapter(messageListAdapter);
updateMessages();
+ this.messagesLoaded = true;
int size = this.messageList.size();
if (size > 0) {
messagesView.setSelection(size - 1);
@@ -843,7 +961,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} catch (final NoSuchElementException ignored) {
}
- activity.xmppConnectionService.updateConversationUi();
+ activity.refreshUi();
}
});
}
@@ -1001,7 +1119,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
this.mSendButton.setImageResource(getSendButtonImageResource(action, status));
}
- public void updateStatusMessages() {
+ protected void updateStatusMessages() {
synchronized (this.messageList) {
if (conversation.getMode() == Conversation.MODE_SINGLE) {
ChatState state = conversation.getIncomingChatState();
@@ -1146,6 +1264,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
builder.create().show();
}
+ protected void sendAxolotlMessage(final Message message) {
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ final XmppConnectionService xmppService = activity.xmppConnectionService;
+ xmppService.sendMessage(message);
+ messageSent();
+ }
+
protected void sendOtrMessage(final Message message) {
final ConversationActivity activity = (ConversationActivity) getActivity();
final XmppConnectionService xmppService = activity.xmppConnectionService;
@@ -1208,6 +1333,21 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
updateSendButton();
}
+ @Override
+ public void onActivityResult(int requestCode, int resultCode,
+ final Intent data) {
+ if (resultCode == Activity.RESULT_OK) {
+ if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
+ final String body = mEditMessage.getText().toString();
+ Message message = new Message(conversation, body, conversation.getNextEncryption());
+ sendAxolotlMessage(message);
+ } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) {
+ int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
+ activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption());
+ }
+ }
+ }
+
private void changeEmojiKeyboardIcon(ImageView iconToBeChanged, int drawableResourceId){
iconToBeChanged.setImageResource(drawableResourceId);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
index 908c29d2..02b1d873 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -1,6 +1,8 @@
package eu.siacs.conversations.ui;
+import android.app.AlertDialog.Builder;
import android.app.PendingIntent;
+import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
@@ -23,18 +25,24 @@ import android.widget.TableLayout;
import android.widget.TextView;
import android.widget.Toast;
+import org.whispersystems.libaxolotl.IdentityKey;
+
+import java.util.Set;
+
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.XmppConnection.Features;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar;
-public class EditAccountActivity extends XmppActivity implements OnAccountUpdate{
+public class EditAccountActivity extends XmppActivity implements OnAccountUpdate, OnKeyStatusUpdated {
private AutoCompleteTextView mAccountJid;
private EditText mPassword;
@@ -52,14 +60,23 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private TextView mServerInfoCSI;
private TextView mServerInfoBlocking;
private TextView mServerInfoPep;
+ private TextView mServerInfoHttpUpload;
private TextView mSessionEst;
private TextView mOtrFingerprint;
+ private TextView mAxolotlFingerprint;
+ private TextView mAccountJidLabel;
private ImageView mAvatar;
private RelativeLayout mOtrFingerprintBox;
+ private RelativeLayout mAxolotlFingerprintBox;
private ImageButton mOtrFingerprintToClipboardButton;
+ private ImageButton mAxolotlFingerprintToClipboardButton;
+ private ImageButton mRegenerateAxolotlKeyButton;
+ private LinearLayout keys;
+ private LinearLayout keysCard;
private Jid jidToEdit;
private Account mAccount;
+ private String messageFingerprint;
private boolean mFetchingAvatar = false;
@@ -72,17 +89,34 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
xmppConnectionService.updateAccount(mAccount);
return;
}
- final boolean registerNewAccount = mRegisterNew.isChecked();
+ final boolean registerNewAccount = mRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI;
+ if (Config.DOMAIN_LOCK != null && mAccountJid.getText().toString().contains("@")) {
+ mAccountJid.setError(getString(R.string.invalid_username));
+ mAccountJid.requestFocus();
+ return;
+ }
final Jid jid;
try {
- jid = Jid.fromString(mAccountJid.getText().toString());
+ if (Config.DOMAIN_LOCK != null) {
+ jid = Jid.fromParts(mAccountJid.getText().toString(),Config.DOMAIN_LOCK,null);
+ } else {
+ jid = Jid.fromString(mAccountJid.getText().toString());
+ }
} catch (final InvalidJidException e) {
- mAccountJid.setError(getString(R.string.invalid_jid));
+ if (Config.DOMAIN_LOCK != null) {
+ mAccountJid.setError(getString(R.string.invalid_username));
+ } else {
+ mAccountJid.setError(getString(R.string.invalid_jid));
+ }
mAccountJid.requestFocus();
return;
}
if (jid.isDomainJid()) {
- mAccountJid.setError(getString(R.string.invalid_jid));
+ if (Config.DOMAIN_LOCK != null) {
+ mAccountJid.setError(getString(R.string.invalid_username));
+ } else {
+ mAccountJid.setError(getString(R.string.invalid_jid));
+ }
mAccountJid.requestFocus();
return;
}
@@ -108,13 +142,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
xmppConnectionService.updateAccount(mAccount);
} else {
- try {
- if (xmppConnectionService.findAccountByJid(Jid.fromString(mAccountJid.getText().toString())) != null) {
- mAccountJid.setError(getString(R.string.account_already_exists));
- mAccountJid.requestFocus();
- return;
- }
- } catch (final InvalidJidException e) {
+ if (xmppConnectionService.findAccountByJid(jid) != null) {
+ mAccountJid.setError(getString(R.string.account_already_exists));
+ mAccountJid.requestFocus();
return;
}
mAccount = new Account(jid.toBareJid(), password);
@@ -139,34 +169,33 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
finish();
}
};
- @Override
- public void onAccountUpdate() {
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- invalidateOptionsMenu();
- if (mAccount != null
- && mAccount.getStatus() != Account.State.ONLINE
- && mFetchingAvatar) {
- startActivity(new Intent(getApplicationContext(),
- ManageAccountActivity.class));
- finish();
- } else if (jidToEdit == null && mAccount != null
- && mAccount.getStatus() == Account.State.ONLINE) {
- if (!mFetchingAvatar) {
- mFetchingAvatar = true;
- xmppConnectionService.checkForAvatar(mAccount,
- mAvatarFetchCallback);
- }
- } else {
- updateSaveButton();
- }
- if (mAccount != null) {
- updateAccountInformation(false);
- }
+ public void refreshUiReal() {
+ invalidateOptionsMenu();
+ if (mAccount != null
+ && mAccount.getStatus() != Account.State.ONLINE
+ && mFetchingAvatar) {
+ startActivity(new Intent(getApplicationContext(),
+ ManageAccountActivity.class));
+ finish();
+ } else if (jidToEdit == null && mAccount != null
+ && mAccount.getStatus() == Account.State.ONLINE) {
+ if (!mFetchingAvatar) {
+ mFetchingAvatar = true;
+ xmppConnectionService.checkForAvatar(mAccount,
+ mAvatarFetchCallback);
}
- });
+ } else {
+ updateSaveButton();
+ }
+ if (mAccount != null) {
+ updateAccountInformation(false);
+ }
+ }
+
+ @Override
+ public void onAccountUpdate() {
+ refreshUi();
}
private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
@@ -271,10 +300,17 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
protected boolean accountInfoEdited() {
- return this.mAccount != null && (!this.mAccount.getJid().toBareJid().toString().equals(
- this.mAccountJid.getText().toString())
- || !this.mAccount.getPassword().equals(
- this.mPassword.getText().toString()));
+ if (this.mAccount == null) {
+ return false;
+ }
+ final String unmodified;
+ if (Config.DOMAIN_LOCK != null) {
+ unmodified = this.mAccount.getJid().getLocalpart();
+ } else {
+ unmodified = this.mAccount.getJid().toBareJid().toString();
+ }
+ return !unmodified.equals(this.mAccountJid.getText().toString()) ||
+ !this.mAccount.getPassword().equals(this.mPassword.getText().toString());
}
@Override
@@ -292,6 +328,11 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
setContentView(R.layout.activity_edit_account);
this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid);
this.mAccountJid.addTextChangedListener(this.mTextWatcher);
+ this.mAccountJidLabel = (TextView) findViewById(R.id.account_jid_label);
+ if (Config.DOMAIN_LOCK != null) {
+ this.mAccountJidLabel.setText(R.string.username);
+ this.mAccountJid.setHint(R.string.username_hint);
+ }
this.mPassword = (EditText) findViewById(R.id.account_password);
this.mPassword.addTextChangedListener(this.mTextWatcher);
this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm);
@@ -307,9 +348,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mServerInfoBlocking = (TextView) findViewById(R.id.server_info_blocking);
this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm);
this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep);
+ this.mServerInfoHttpUpload = (TextView) findViewById(R.id.server_info_http_upload);
this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint);
this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box);
this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard);
+ this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint);
+ this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box);
+ this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard);
+ this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key);
+ this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card);
+ this.keys = (LinearLayout) findViewById(R.id.other_device_keys);
this.mSaveButton = (Button) findViewById(R.id.save_button);
this.mCancelButton = (Button) findViewById(R.id.cancel_button);
this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener);
@@ -328,6 +376,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
};
this.mRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword);
+ if (Config.DISALLOW_REGISTRATION_IN_UI) {
+ this.mRegisterNew.setVisibility(View.GONE);
+ }
}
@Override
@@ -338,6 +389,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
+ final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices);
if (mAccount != null && mAccount.isOnlineAndConnected()) {
if (!mAccount.getXmppConnection().getFeatures().blocking()) {
showBlocklist.setVisible(false);
@@ -345,11 +397,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
if (!mAccount.getXmppConnection().getFeatures().register()) {
changePassword.setVisible(false);
}
+ Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
+ if (otherDevices == null || otherDevices.isEmpty()) {
+ clearDevices.setVisible(false);
+ }
} else {
showQrCode.setVisible(false);
showBlocklist.setVisible(false);
showMoreInfo.setVisible(false);
changePassword.setVisible(false);
+ clearDevices.setVisible(false);
}
return true;
}
@@ -363,6 +420,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} catch (final InvalidJidException | NullPointerException ignored) {
this.jidToEdit = null;
}
+ this.messageFingerprint = getIntent().getStringExtra("fingerprint");
if (this.jidToEdit != null) {
this.mRegisterNew.setVisibility(View.GONE);
if (getActionBar() != null) {
@@ -379,9 +437,6 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
protected void onBackendConnected() {
- final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this,
- android.R.layout.simple_list_item_1,
- xmppConnectionService.getKnownHosts());
if (this.jidToEdit != null) {
this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
updateAccountInformation(true);
@@ -394,7 +449,12 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mCancelButton.setEnabled(false);
this.mCancelButton.setTextColor(getSecondaryTextColor());
}
- this.mAccountJid.setAdapter(mKnownHostsAdapter);
+ if (Config.DOMAIN_LOCK == null) {
+ final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this,
+ android.R.layout.simple_list_item_1,
+ xmppConnectionService.getKnownHosts());
+ this.mAccountJid.setAdapter(mKnownHostsAdapter);
+ }
updateSaveButton();
}
@@ -415,13 +475,20 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
changePasswordIntent.putExtra("account", mAccount.getJid().toString());
startActivity(changePasswordIntent);
break;
+ case R.id.action_clear_devices:
+ showWipePepDialog();
+ break;
}
return super.onOptionsItemSelected(item);
}
private void updateAccountInformation(boolean init) {
if (init) {
- this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString());
+ if (Config.DOMAIN_LOCK != null) {
+ this.mAccountJid.setText(this.mAccount.getJid().getLocalpart());
+ } else {
+ this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString());
+ }
this.mPassword.setText(this.mAccount.getPassword());
}
if (this.jidToEdit != null) {
@@ -477,10 +544,15 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} else {
this.mServerInfoPep.setText(R.string.server_info_unavailable);
}
- final String fingerprint = this.mAccount.getOtrFingerprint();
- if (fingerprint != null) {
+ if (features.httpUpload()) {
+ this.mServerInfoHttpUpload.setText(R.string.server_info_available);
+ } else {
+ this.mServerInfoHttpUpload.setText(R.string.server_info_unavailable);
+ }
+ final String otrFingerprint = this.mAccount.getOtrFingerprint();
+ if (otrFingerprint != null) {
this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
- this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint));
+ this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
this.mOtrFingerprintToClipboardButton
.setVisibility(View.VISIBLE);
this.mOtrFingerprintToClipboardButton
@@ -489,7 +561,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
public void onClick(final View v) {
- if (copyTextToClipboard(fingerprint, R.string.otr_fingerprint)) {
+ if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_otr_fingerprint,
@@ -500,6 +572,57 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} else {
this.mOtrFingerprintBox.setVisibility(View.GONE);
}
+ final String axolotlFingerprint = this.mAccount.getAxolotlService().getOwnPublicKey().getFingerprint();
+ if (axolotlFingerprint != null) {
+ this.mAxolotlFingerprintBox.setVisibility(View.VISIBLE);
+ this.mAxolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(axolotlFingerprint));
+ this.mAxolotlFingerprintToClipboardButton
+ .setVisibility(View.VISIBLE);
+ this.mAxolotlFingerprintToClipboardButton
+ .setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+
+ if (copyTextToClipboard(axolotlFingerprint, R.string.omemo_fingerprint)) {
+ Toast.makeText(
+ EditAccountActivity.this,
+ R.string.toast_message_omemo_fingerprint,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ if (Config.SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON) {
+ this.mRegenerateAxolotlKeyButton
+ .setVisibility(View.VISIBLE);
+ this.mRegenerateAxolotlKeyButton
+ .setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(final View v) {
+ showRegenerateAxolotlKeyDialog();
+ }
+ });
+ }
+ } else {
+ this.mAxolotlFingerprintBox.setVisibility(View.GONE);
+ }
+ final IdentityKey ownKey = mAccount.getAxolotlService().getOwnPublicKey();
+ boolean hasKeys = false;
+ keys.removeAllViews();
+ for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
+ mAccount, mAccount.getJid().toBareJid().toString())) {
+ if(ownKey.equals(identityKey)) {
+ continue;
+ }
+ boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint);
+ hasKeys |= addFingerprintRow(keys, mAccount, identityKey, highlight);
+ }
+ if (hasKeys) {
+ keysCard.setVisibility(View.VISIBLE);
+ } else {
+ keysCard.setVisibility(View.GONE);
+ }
} else {
if (this.mAccount.errorStatus()) {
this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId()));
@@ -512,4 +635,41 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mStats.setVisibility(View.GONE);
}
}
+
+ public void showRegenerateAxolotlKeyDialog() {
+ Builder builder = new Builder(this);
+ builder.setTitle("Regenerate Key");
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)");
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton("Yes",
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mAccount.getAxolotlService().regenerateKeys();
+ }
+ });
+ builder.create().show();
+ }
+
+ public void showWipePepDialog() {
+ Builder builder = new Builder(this);
+ builder.setTitle(getString(R.string.clear_other_devices));
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage(getString(R.string.clear_other_devices_desc));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.accept),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mAccount.getAxolotlService().wipeOtherPepDevices();
+ }
+ });
+ builder.create().show();
+ }
+
+ @Override
+ public void onKeyStatusUpdated() {
+ refreshUi();
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
index 56dbc55e..5b9b7355 100644
--- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -1,27 +1,29 @@
package eu.siacs.conversations.ui;
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
-import eu.siacs.conversations.ui.adapter.AccountAdapter;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
-import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
+import eu.siacs.conversations.ui.adapter.AccountAdapter;
+
public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate {
protected Account selectedAccount = null;
@@ -80,6 +82,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.setHeaderTitle(this.selectedAccount.getJid().toBareJid().toString());
}
diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
index 4333dbdb..e01490f9 100644
--- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
@@ -13,6 +13,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.PhoneHelper;
@@ -192,7 +193,13 @@ public class PublishProfilePictureActivity extends XmppActivity {
} else {
loadImageIntoPreview(avatarUri);
}
- this.accountTextView.setText(this.account.getJid().toBareJid().toString());
+ String account;
+ if (Config.DOMAIN_LOCK != null) {
+ account = this.account.getJid().getLocalpart();
+ } else {
+ account = this.account.getJid().toBareJid().toString();
+ }
+ this.accountTextView.setText(account);
}
}
@@ -251,4 +258,8 @@ public class PublishProfilePictureActivity extends XmppActivity {
this.publishButton.setTextColor(getSecondaryTextColor());
}
+ public void refreshUiReal() {
+ //nothing to do. This Activity doesn't implement any listeners
+ }
+
}
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index df2933c9..f9238f13 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -142,13 +142,14 @@ public class SettingsActivity extends XmppActivity implements
.toLowerCase(Locale.US);
if (xmppConnectionServiceBound) {
for (Account account : xmppConnectionService.getAccounts()) {
- account.setResource(resource);
- if (!account.isOptionSet(Account.OPTION_DISABLED)) {
- XmppConnection connection = account.getXmppConnection();
- if (connection != null) {
- connection.resetStreamId();
- }
- xmppConnectionService.reconnectAccountInBackground(account);
+ if (account.setResource(resource)) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.resetStreamId();
+ }
+ xmppConnectionService.reconnectAccountInBackground(account);
+ }
}
}
}
@@ -192,4 +193,8 @@ public class SettingsActivity extends XmppActivity implements
}
}
+ public void refreshUiReal() {
+ //nothing to do. This Activity doesn't implement any listeners
+ }
+
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
index 351f1dfc..e0ebd5c2 100644
--- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java
@@ -17,7 +17,6 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
-import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
@@ -229,4 +228,8 @@ public class ShareWithActivity extends XmppActivity {
}
+ public void refreshUiReal() {
+ //nothing to do. This Activity doesn't implement any listeners
+ }
+
}
diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
index 68e77af4..74621fc4 100644
--- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
@@ -289,7 +289,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
protected void toggleContactBlock() {
final int position = contact_context_id;
- BlockContactDialog.show(this, xmppConnectionService, (Contact)contacts.get(position));
+ BlockContactDialog.show(this, xmppConnectionService, (Contact) contacts.get(position));
}
protected void deleteContact() {
@@ -299,7 +299,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
builder.setNegativeButton(R.string.cancel, null);
builder.setTitle(R.string.action_delete_contact);
builder.setMessage(getString(R.string.remove_contact_text,
- contact.getJid()));
+ contact.getJid()));
builder.setPositiveButton(R.string.delete, new OnClickListener() {
@Override
@@ -319,7 +319,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
builder.setNegativeButton(R.string.cancel, null);
builder.setTitle(R.string.delete_bookmark);
builder.setMessage(getString(R.string.remove_bookmark_text,
- bookmark.getJid()));
+ bookmark.getJid()));
builder.setPositiveButton(R.string.delete, new OnClickListener() {
@Override
@@ -368,7 +368,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
}
final Jid accountJid;
try {
- accountJid = Jid.fromString((String) spinner.getSelectedItem());
+ if (Config.DOMAIN_LOCK != null) {
+ accountJid = Jid.fromParts((String) spinner.getSelectedItem(),Config.DOMAIN_LOCK,null);
+ } else {
+ accountJid = Jid.fromString((String) spinner.getSelectedItem());
+ }
} catch (final InvalidJidException e) {
return;
}
@@ -379,8 +383,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
jid.setError(getString(R.string.invalid_jid));
return;
}
- final Account account = xmppConnectionService
- .findAccountByJid(accountJid);
+ final Account account = xmppConnectionService.findAccountByJid(accountJid);
if (account == null) {
dialog.dismiss();
return;
@@ -428,7 +431,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
}
final Jid accountJid;
try {
- accountJid = Jid.fromString((String) spinner.getSelectedItem());
+ if (Config.DOMAIN_LOCK != null) {
+ accountJid = Jid.fromParts((String) spinner.getSelectedItem(),Config.DOMAIN_LOCK,null);
+ } else {
+ accountJid = Jid.fromString((String) spinner.getSelectedItem());
+ }
} catch (final InvalidJidException e) {
return;
}
@@ -576,7 +583,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
this.mActivatedAccounts.clear();
for (Account account : xmppConnectionService.getAccounts()) {
if (account.getStatus() != Account.State.DISABLED) {
- this.mActivatedAccounts.add(account.getJid().toBareJid().toString());
+ if (Config.DOMAIN_LOCK != null) {
+ this.mActivatedAccounts.add(account.getJid().getLocalpart());
+ } else {
+ this.mActivatedAccounts.add(account.getJid().toBareJid().toString());
+ }
}
}
final Intent intent = getIntent();
diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
new file mode 100644
index 00000000..0e685c3e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
@@ -0,0 +1,265 @@
+package eu.siacs.conversations.ui;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.whispersystems.libaxolotl.IdentityKey;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import eu.siacs.conversations.R;
+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 boolean hasOtherTrustedKeys = false;
+ private boolean hasPendingFetches = false;
+ private boolean hasNoTrustedKeys = true;
+
+ private Contact contact;
+ 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 final Map<IdentityKey, Boolean> ownKeysToTrust = new HashMap<>();
+ private final Map<IdentityKey, Boolean> foreignKeysToTrust = new HashMap<>();
+
+ private final OnClickListener mSaveButtonListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ commitTrusts();
+ Intent data = new Intent();
+ data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ };
+
+ private final OnClickListener mCancelButtonListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ };
+
+ @Override
+ protected void refreshUiReal() {
+ invalidateOptionsMenu();
+ populateView();
+ }
+
+ @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("account"));
+ } catch (final InvalidJidException ignored) {
+ }
+ try {
+ this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact"));
+ } catch (final InvalidJidException ignored) {
+ }
+ hasNoTrustedKeys = getIntent().getBooleanExtra("has_no_trusted", false);
+
+ keyErrorMessageCard = (LinearLayout) findViewById(R.id.key_error_message_card);
+ keyErrorMessage = (TextView) findViewById(R.id.key_error_message);
+ 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);
+ mCancelButton = (Button) findViewById(R.id.cancel_button);
+ mCancelButton.setOnClickListener(mCancelButtonListener);
+ mSaveButton = (Button) findViewById(R.id.save_button);
+ mSaveButton.setOnClickListener(mSaveButtonListener);
+
+
+ if (getActionBar() != null) {
+ getActionBar().setHomeButtonEnabled(true);
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ private void populateView() {
+ setTitle(getString(R.string.trust_omemo_fingerprints));
+ ownKeys.removeAllViews();
+ foreignKeys.removeAllViews();
+ boolean hasOwnKeys = false;
+ boolean hasForeignKeys = false;
+ for(final IdentityKey identityKey : ownKeysToTrust.keySet()) {
+ hasOwnKeys = true;
+ addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey, false,
+ XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false,
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ ownKeysToTrust.put(identityKey, isChecked);
+ // own fingerprints have no impact on locked status.
+ }
+ },
+ null
+ );
+ }
+ for(final IdentityKey identityKey : foreignKeysToTrust.keySet()) {
+ hasForeignKeys = true;
+ addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey, false,
+ XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false,
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ foreignKeysToTrust.put(identityKey, isChecked);
+ lockOrUnlockAsNeeded();
+ }
+ },
+ null
+ );
+ }
+
+ if(hasOwnKeys) {
+ ownKeysTitle.setText(accountJid.toString());
+ ownKeysCard.setVisibility(View.VISIBLE);
+ }
+ if(hasForeignKeys) {
+ foreignKeysTitle.setText(contactJid.toString());
+ foreignKeysCard.setVisibility(View.VISIBLE);
+ }
+ if(hasPendingFetches) {
+ setFetching();
+ lock();
+ } else {
+ if (!hasForeignKeys && !hasOtherTrustedKeys) {
+ keyErrorMessageCard.setVisibility(View.VISIBLE);
+ keyErrorMessage.setText(R.string.error_no_keys_to_trust);
+ ownKeys.removeAllViews(); ownKeysCard.setVisibility(View.GONE);
+ foreignKeys.removeAllViews(); foreignKeysCard.setVisibility(View.GONE);
+ }
+ lockOrUnlockAsNeeded();
+ setDone();
+ }
+ }
+
+ private void getFingerprints(final Account account) {
+ Set<IdentityKey> ownKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
+ Set<IdentityKey> foreignKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, contact);
+ if (hasNoTrustedKeys) {
+ ownKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED));
+ foreignKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, contact));
+ }
+ for(final IdentityKey identityKey : ownKeysSet) {
+ if(!ownKeysToTrust.containsKey(identityKey)) {
+ ownKeysToTrust.put(identityKey, false);
+ }
+ }
+ for(final IdentityKey identityKey : foreignKeysSet) {
+ if(!foreignKeysToTrust.containsKey(identityKey)) {
+ foreignKeysToTrust.put(identityKey, false);
+ }
+ }
+ }
+
+ @Override
+ public void onBackendConnected() {
+ if ((accountJid != null) && (contactJid != null)) {
+ final Account account = xmppConnectionService
+ .findAccountByJid(accountJid);
+ if (account == null) {
+ return;
+ }
+ this.contact = account.getRoster().getContact(contactJid);
+ ownKeysToTrust.clear();
+ foreignKeysToTrust.clear();
+ getFingerprints(account);
+
+ if(account.getAxolotlService().getNumTrustedKeys(contact) > 0) {
+ hasOtherTrustedKeys = true;
+ }
+ Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, false);
+ if(account.getAxolotlService().hasPendingKeyFetches(conversation)) {
+ hasPendingFetches = true;
+ }
+
+ populateView();
+ }
+ }
+
+ @Override
+ public void onKeyStatusUpdated() {
+ final Account account = xmppConnectionService.findAccountByJid(accountJid);
+ hasPendingFetches = false;
+ getFingerprints(account);
+ refreshUi();
+ }
+
+ private void commitTrusts() {
+ for(IdentityKey identityKey:ownKeysToTrust.keySet()) {
+ contact.getAccount().getAxolotlService().setFingerprintTrust(
+ identityKey.getFingerprint().replaceAll("\\s", ""),
+ XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey)));
+ }
+ for(IdentityKey identityKey:foreignKeysToTrust.keySet()) {
+ contact.getAccount().getAxolotlService().setFingerprintTrust(
+ identityKey.getFingerprint().replaceAll("\\s", ""),
+ XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey)));
+ }
+ }
+
+ private void unlock() {
+ mSaveButton.setEnabled(true);
+ mSaveButton.setTextColor(getPrimaryTextColor());
+ }
+
+ private void lock() {
+ mSaveButton.setEnabled(false);
+ mSaveButton.setTextColor(getSecondaryTextColor());
+ }
+
+ private void lockOrUnlockAsNeeded() {
+ if (!hasOtherTrustedKeys && !foreignKeysToTrust.values().contains(true)){
+ lock();
+ } else {
+ unlock();
+ }
+ }
+
+ private void setDone() {
+ mSaveButton.setText(getString(R.string.done));
+ }
+
+ private void setFetching() {
+ mSaveButton.setText(getString(R.string.fetching_keys));
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
index 4157035b..2d780eda 100644
--- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -43,8 +43,11 @@ import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
+import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
@@ -56,6 +59,8 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import net.java.otr4j.session.SessionID;
+import org.whispersystems.libaxolotl.IdentityKey;
+
import java.io.FileNotFoundException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -66,6 +71,7 @@ import java.util.concurrent.RejectedExecutionException;
import de.tzur.conversations.Settings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
+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;
@@ -75,7 +81,10 @@ import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
+import eu.siacs.conversations.ui.widget.Switch;
+import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper;
+import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@@ -91,6 +100,7 @@ public abstract class XmppActivity extends Activity {
protected int mPrimaryTextColor;
protected int mSecondaryTextColor;
+ protected int mTertiaryTextColor;
protected int mPrimaryBackgroundColor;
protected int mSecondaryBackgroundColor;
protected int mColorRed;
@@ -117,7 +127,7 @@ public abstract class XmppActivity extends Activity {
protected ConferenceInvite mPendingConferenceInvite = null;
- protected void refreshUi() {
+ protected final void refreshUi() {
final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
if (diff > Config.REFRESH_UI_INTERVAL) {
mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
@@ -129,9 +139,7 @@ public abstract class XmppActivity extends Activity {
}
}
- protected void refreshUiReal() {
-
- };
+ abstract protected void refreshUiReal();
protected interface OnValueEdited {
public void onValueEdited(String value);
@@ -288,6 +296,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnShowErrorToast) {
this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
}
+ if (this instanceof OnKeyStatusUpdated) {
+ this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
+ }
}
protected void unregisterListeners() {
@@ -309,6 +320,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnShowErrorToast) {
this.xmppConnectionService.removeOnShowErrorToastListener();
}
+ if (this instanceof OnKeyStatusUpdated) {
+ this.xmppConnectionService.removeOnNewKeysAvailableListener();
+ }
}
@Override
@@ -337,7 +351,8 @@ public abstract class XmppActivity extends Activity {
ExceptionHelper.init(getApplicationContext());
mPrimaryTextColor = getResources().getColor(R.color.black87);
mSecondaryTextColor = getResources().getColor(R.color.black54);
- mColorRed = getResources().getColor(R.color.red500);
+ mTertiaryTextColor = getResources().getColor(R.color.black12);
+ mColorRed = getResources().getColor(R.color.red800);
mColorOrange = getResources().getColor(R.color.orange500);
mColorGreen = getResources().getColor(R.color.green500);
mPrimaryColor = getResources().getColor(R.color.green500);
@@ -375,14 +390,18 @@ public abstract class XmppActivity extends Activity {
public void switchToConversation(Conversation conversation, String text,
boolean newTask) {
- switchToConversation(conversation,text,null,newTask);
+ switchToConversation(conversation,text,null,false,newTask);
}
public void highlightInMuc(Conversation conversation, String nick) {
- switchToConversation(conversation, null, nick, false);
+ switchToConversation(conversation, null, nick, false, false);
+ }
+
+ public void privateMsgInMuc(Conversation conversation, String nick) {
+ switchToConversation(conversation, null, nick, true, false);
}
- private void switchToConversation(Conversation conversation, String text, String nick, boolean newTask) {
+ private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) {
Intent viewConversationIntent = new Intent(this,
ConversationActivity.class);
viewConversationIntent.setAction(Intent.ACTION_VIEW);
@@ -393,6 +412,7 @@ public abstract class XmppActivity extends Activity {
}
if (nick != null) {
viewConversationIntent.putExtra(ConversationActivity.NICK, nick);
+ viewConversationIntent.putExtra(ConversationActivity.PRIVATE_MESSAGE,pm);
}
viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
if (newTask) {
@@ -408,10 +428,15 @@ public abstract class XmppActivity extends Activity {
}
public void switchToContactDetails(Contact contact) {
+ switchToContactDetails(contact, null);
+ }
+
+ public void switchToContactDetails(Contact contact, String messageFingerprint) {
Intent intent = new Intent(this, ContactDetailsActivity.class);
intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
intent.putExtra("account", contact.getAccount().getJid().toBareJid().toString());
intent.putExtra("contact", contact.getJid().toString());
+ intent.putExtra("fingerprint", messageFingerprint);
startActivity(intent);
}
@@ -592,6 +617,124 @@ public abstract class XmppActivity extends Activity {
builder.create().show();
}
+ protected boolean addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey, boolean highlight) {
+ final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
+ final XmppAxolotlSession.Trust trust = account.getAxolotlService()
+ .getFingerprintTrust(fingerprint);
+ return addFingerprintRowWithListeners(keys, account, identityKey, highlight, trust, true,
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ account.getAxolotlService().setFingerprintTrust(fingerprint,
+ (isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
+ XmppAxolotlSession.Trust.UNTRUSTED);
+ }
+ },
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ account.getAxolotlService().setFingerprintTrust(fingerprint,
+ XmppAxolotlSession.Trust.UNTRUSTED);
+ v.setEnabled(true);
+ }
+ }
+
+ );
+ }
+
+ protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
+ final IdentityKey identityKey,
+ boolean highlight,
+ XmppAxolotlSession.Trust trust,
+ boolean showTag,
+ CompoundButton.OnCheckedChangeListener
+ onCheckedChangeListener,
+ View.OnClickListener onClickListener) {
+ if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
+ return false;
+ }
+ View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
+ TextView key = (TextView) view.findViewById(R.id.key);
+ TextView keyType = (TextView) view.findViewById(R.id.key_type);
+ Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
+ trustToggle.setVisibility(View.VISIBLE);
+ trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
+ trustToggle.setOnClickListener(onClickListener);
+ view.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ showPurgeKeyDialog(account, identityKey);
+ return true;
+ }
+ });
+
+ switch (trust) {
+ case UNTRUSTED:
+ case TRUSTED:
+ trustToggle.setChecked(trust == XmppAxolotlSession.Trust.TRUSTED, false);
+ trustToggle.setEnabled(true);
+ key.setTextColor(getPrimaryTextColor());
+ keyType.setTextColor(getSecondaryTextColor());
+ break;
+ case UNDECIDED:
+ trustToggle.setChecked(false, false);
+ trustToggle.setEnabled(false);
+ key.setTextColor(getPrimaryTextColor());
+ keyType.setTextColor(getSecondaryTextColor());
+ break;
+ case INACTIVE_UNTRUSTED:
+ case INACTIVE_UNDECIDED:
+ trustToggle.setOnClickListener(null);
+ trustToggle.setChecked(false, false);
+ trustToggle.setEnabled(false);
+ key.setTextColor(getTertiaryTextColor());
+ keyType.setTextColor(getTertiaryTextColor());
+ break;
+ case INACTIVE_TRUSTED:
+ trustToggle.setOnClickListener(null);
+ trustToggle.setChecked(true, false);
+ trustToggle.setEnabled(false);
+ key.setTextColor(getTertiaryTextColor());
+ keyType.setTextColor(getTertiaryTextColor());
+ break;
+ }
+
+ if (showTag) {
+ keyType.setText(getString(R.string.omemo_fingerprint));
+ } else {
+ keyType.setVisibility(View.GONE);
+ }
+ if (highlight) {
+ keyType.setTextColor(getResources().getColor(R.color.accent));
+ keyType.setText(getString(R.string.omemo_fingerprint_selected_message));
+ } else {
+ keyType.setText(getString(R.string.omemo_fingerprint));
+ }
+
+ key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint()));
+ keys.addView(view);
+ return true;
+ }
+
+ public void showPurgeKeyDialog(final Account account, final IdentityKey identityKey) {
+ Builder builder = new Builder(this);
+ builder.setTitle(getString(R.string.purge_key));
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage(getString(R.string.purge_key_desc_part1)
+ + "\n\n" + CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())
+ + "\n\n" + getString(R.string.purge_key_desc_part2));
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ builder.setPositiveButton(getString(R.string.accept),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ account.getAxolotlService().purgeKey(identityKey);
+ refreshUi();
+ }
+ });
+ builder.create().show();
+ }
+
public void selectPresence(final Conversation conversation,
final OnPresenceSelected listener) {
final Contact contact = conversation.getContact();
@@ -711,6 +854,10 @@ public abstract class XmppActivity extends Activity {
}
};
+ public int getTertiaryTextColor() {
+ return this.mTertiaryTextColor;
+ }
+
public int getSecondaryTextColor() {
return this.mSecondaryTextColor;
}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
index 782a1231..141dbc3a 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
@@ -1,11 +1,5 @@
package eu.siacs.conversations.ui.adapter;
-import java.util.List;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.ui.XmppActivity;
-import eu.siacs.conversations.ui.ManageAccountActivity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
@@ -16,6 +10,15 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Switch;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.ui.ManageAccountActivity;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.widget.Switch;
+
public class AccountAdapter extends ArrayAdapter<Account> {
private XmppActivity activity;
@@ -34,7 +37,11 @@ public class AccountAdapter extends ArrayAdapter<Account> {
view = inflater.inflate(R.layout.account_row, parent, false);
}
TextView jid = (TextView) view.findViewById(R.id.account_jid);
- jid.setText(account.getJid().toBareJid().toString());
+ if (Config.DOMAIN_LOCK != null) {
+ jid.setText(account.getJid().getLocalpart());
+ } else {
+ jid.setText(account.getJid().toBareJid().toString());
+ }
TextView statusView = (TextView) view.findViewById(R.id.account_status);
ImageView imageView = (ImageView) view.findViewById(R.id.account_image);
imageView.setImageBitmap(activity.avatarService().get(account, activity.getPixel(48)));
@@ -53,8 +60,7 @@ public class AccountAdapter extends ArrayAdapter<Account> {
}
final Switch tglAccountState = (Switch) view.findViewById(R.id.tgl_account_status);
final boolean isDisabled = (account.getStatus() == Account.State.DISABLED);
- tglAccountState.setOnCheckedChangeListener(null);
- tglAccountState.setChecked(!isDisabled);
+ tglAccountState.setChecked(!isDisabled,false);
tglAccountState.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
index 7443c0cb..b89dd01a 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
@@ -24,9 +24,8 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.UIHelper;
@@ -119,8 +118,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
Pair<String,Boolean> preview = UIHelper.getMessagePreview(activity,message);
mLastMessage.setVisibility(View.VISIBLE);
imagePreview.setVisibility(View.GONE);
- CharSequence msgText = preview.first;
- mLastMessage.setText(msgText);
+ mLastMessage.setText(preview.first);
if (preview.second) {
if (conversation.isRead()) {
mLastMessage.setTypeface(null, Typeface.ITALIC);
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
index 0993735f..471526af 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java
@@ -1,13 +1,13 @@
package eu.siacs.conversations.ui.adapter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.Filter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
public class KnownHostsAdapter extends ArrayAdapter<String> {
private ArrayList<String> domains;
private Filter domainFilter = new Filter() {
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 b6979cc6..a5b25967 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -3,6 +3,7 @@ package eu.siacs.conversations.ui.adapter;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.preference.PreferenceManager;
@@ -29,13 +30,14 @@ import java.util.List;
import de.tzur.conversations.Settings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
+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.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Message.FileParams;
+import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper;
@@ -83,18 +85,30 @@ public class MessageAdapter extends ArrayAdapter<Message> {
return 3;
}
- @Override
- public int getItemViewType(int position) {
- if (getItem(position).getType() == Message.TYPE_STATUS) {
+ public int getItemViewType(Message message) {
+ if (message.getType() == Message.TYPE_STATUS) {
return STATUS;
- } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) {
+ } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
return RECEIVED;
+ }
+
+ return SENT;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return this.getItemViewType(getItem(position));
+ }
+
+ private int getMessageTextColor(int type, boolean primary) {
+ if (type == SENT) {
+ return activity.getResources().getColor(primary ? R.color.black87 : R.color.black54);
} else {
- return SENT;
+ return activity.getResources().getColor(primary ? R.color.white : R.color.white70);
}
}
- private void displayStatus(ViewHolder viewHolder, Message message) {
+ private void displayStatus(ViewHolder viewHolder, Message message, int type) {
String filesize = null;
String info = null;
boolean error = false;
@@ -149,15 +163,39 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
break;
}
- if (error) {
+ if (error && type == SENT) {
viewHolder.time.setTextColor(activity.getWarningTextColor());
} else {
- viewHolder.time.setTextColor(activity.getSecondaryTextColor());
+ viewHolder.time.setTextColor(this.getMessageTextColor(type,false));
}
if (message.getEncryption() == Message.ENCRYPTION_NONE) {
viewHolder.indicator.setVisibility(View.GONE);
} else {
viewHolder.indicator.setVisibility(View.VISIBLE);
+ if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ XmppAxolotlSession.Trust trust = message.getConversation()
+ .getAccount().getAxolotlService().getFingerprintTrust(
+ message.getAxolotlFingerprint());
+
+ if(trust == null || trust != XmppAxolotlSession.Trust.TRUSTED) {
+ viewHolder.indicator.setColorFilter(activity.getWarningTextColor());
+ viewHolder.indicator.setAlpha(1.0f);
+ } else {
+ viewHolder.indicator.clearColorFilter();
+ if (type == SENT) {
+ viewHolder.indicator.setAlpha(0.57f);
+ } else {
+ viewHolder.indicator.setAlpha(0.7f);
+ }
+ }
+ } else {
+ viewHolder.indicator.clearColorFilter();
+ if (type == SENT) {
+ viewHolder.indicator.setAlpha(0.57f);
+ } else {
+ viewHolder.indicator.setAlpha(0.7f);
+ }
+ }
}
String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),
@@ -189,19 +227,19 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
}
- private void displayInfoMessage(ViewHolder viewHolder, String text) {
+ private void displayInfoMessage(ViewHolder viewHolder, String text, int type) {
if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE);
}
viewHolder.image.setVisibility(View.GONE);
viewHolder.messageBody.setVisibility(View.VISIBLE);
viewHolder.messageBody.setText(text);
- viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor());
+ viewHolder.messageBody.setTextColor(getMessageTextColor(type,false));
viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
viewHolder.messageBody.setTextIsSelectable(false);
}
- private void displayDecryptionFailed(ViewHolder viewHolder) {
+ private void displayDecryptionFailed(ViewHolder viewHolder, int type) {
if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE);
}
@@ -209,7 +247,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setVisibility(View.VISIBLE);
viewHolder.messageBody.setText(getContext().getString(
R.string.decryption_failed));
- viewHolder.messageBody.setTextColor(activity.getWarningTextColor());
+ viewHolder.messageBody.setTextColor(getMessageTextColor(type,false));
viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
viewHolder.messageBody.setTextIsSelectable(false);
}
@@ -227,7 +265,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setText(span);
}
- private void displayTextMessage(final ViewHolder viewHolder, final Message message) {
+ private void displayTextMessage(final ViewHolder viewHolder, final Message message, int type) {
if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE);
}
@@ -270,8 +308,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
final Spannable span = new SpannableString(privateMarker + " "
+ formattedBody);
- span.setSpan(new ForegroundColorSpan(activity
- .getSecondaryTextColor()), 0, privateMarker
+ span.setSpan(new ForegroundColorSpan(getMessageTextColor(type,false)), 0, privateMarker
.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
span.setSpan(new StyleSpan(Typeface.BOLD), 0,
privateMarker.length(),
@@ -286,7 +323,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else {
viewHolder.messageBody.setText("");
}
- viewHolder.messageBody.setTextColor(activity.getPrimaryTextColor());
+ viewHolder.messageBody.setTextColor(this.getMessageTextColor(type,true));
viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
viewHolder.messageBody.setTextIsSelectable(true);
}
@@ -355,17 +392,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
scalledW = (int) target;
scalledH = (int) (params.height / ((double) params.width / target));
}
- viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(
- scalledW, scalledH));
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scalledW, scalledH);
+ layoutParams.setMargins(0, (int)(metrics.density * 4), 0, (int)(metrics.density * 4));
+ viewHolder.image.setLayoutParams(layoutParams);
activity.loadBitmap(message, viewHolder.image);
viewHolder.image.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(activity.xmppConnectionService
- .getFileBackend().getJingleFileUri(message), "image/*");
- getContext().startActivity(intent);
+ openDownloadable(message);
}
});
viewHolder.image.setOnLongClickListener(openContextMenu);
@@ -494,7 +529,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)));
} else {
- displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first);
+ displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first,type);
}
} else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
displayImageMessage(viewHolder, message);
@@ -506,10 +541,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
if (activity.hasPgp()) {
- displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message));
+ displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message),type);
} else {
- displayInfoMessage(viewHolder,
- activity.getString(R.string.install_openkeychain));
+ displayInfoMessage(viewHolder,activity.getString(R.string.install_openkeychain),type);
if (viewHolder != null) {
viewHolder.message_box
.setOnClickListener(new OnClickListener() {
@@ -522,18 +556,26 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
}
} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
- displayDecryptionFailed(viewHolder);
+ displayDecryptionFailed(viewHolder,type);
} else {
if (GeoHelper.isGeoUri(message.getBody())) {
displayLocationMessage(viewHolder,message);
} else if (message.treatAsDownloadable() == Message.Decision.MUST) {
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)));
} else {
- displayTextMessage(viewHolder, message);
+ displayTextMessage(viewHolder, message, type);
+ }
+ }
+
+ if (type == RECEIVED) {
+ if(message.isValidInSession()) {
+ viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received);
+ } else {
+ viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning);
}
}
- displayStatus(viewHolder, message);
+ displayStatus(viewHolder, message, type);
return view;
}
@@ -546,7 +588,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
Toast.LENGTH_SHORT).show();
}
} else if (message.treatAsDownloadable() != Message.Decision.NEVER) {
- activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message);
+ activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message,true);
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/widget/Switch.java b/src/main/java/eu/siacs/conversations/ui/widget/Switch.java
new file mode 100644
index 00000000..fd3b5553
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/widget/Switch.java
@@ -0,0 +1,68 @@
+package eu.siacs.conversations.ui.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.kyleduo.switchbutton.SwitchButton;
+
+public class Switch extends SwitchButton {
+
+ private int mTouchSlop;
+ private int mClickTimeout;
+ private float mStartX;
+ private float mStartY;
+ private OnClickListener mOnClickListener;
+
+ public Switch(Context context) {
+ super(context);
+ mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
+ }
+
+ public Switch(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
+ }
+
+ public Switch(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener onClickListener) {
+ this.mOnClickListener = onClickListener;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!isEnabled()) {
+ float deltaX = event.getX() - mStartX;
+ float deltaY = event.getY() - mStartY;
+ int action = event.getAction();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mStartX = event.getX();
+ mStartY = event.getY();
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ float time = event.getEventTime() - event.getDownTime();
+ if (deltaX < mTouchSlop && deltaY < mTouchSlop && time < mClickTimeout) {
+ if (mOnClickListener != null) {
+ this.mOnClickListener.onClick(this);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
index 2dec203d..c7c9ac42 100644
--- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
@@ -96,11 +96,10 @@ public final class CryptoHelper {
} else if (fingerprint.length() < 40) {
return fingerprint;
}
- StringBuilder builder = new StringBuilder(fingerprint);
- builder.insert(8, " ");
- builder.insert(17, " ");
- builder.insert(26, " ");
- builder.insert(35, " ");
+ StringBuilder builder = new StringBuilder(fingerprint.replaceAll("\\s",""));
+ for(int i=8;i<builder.length();i+=9) {
+ builder.insert(i, ' ');
+ }
return builder.toString();
}
diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
index 5a47bb3c..4d0dd3da 100644
--- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
@@ -1,17 +1,7 @@
package eu.siacs.conversations.utils;
-import de.measite.minidns.Client;
-import de.measite.minidns.DNSMessage;
-import de.measite.minidns.Record;
-import de.measite.minidns.Record.TYPE;
-import de.measite.minidns.Record.CLASS;
-import de.measite.minidns.record.SRV;
-import de.measite.minidns.record.A;
-import de.measite.minidns.record.AAAA;
-import de.measite.minidns.record.Data;
-import de.measite.minidns.util.NameUtil;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.xmpp.jid.Jid;
+import android.os.Bundle;
+import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
@@ -22,8 +12,18 @@ import java.util.Random;
import java.util.TreeMap;
import java.util.regex.Pattern;
-import android.os.Bundle;
-import android.util.Log;
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.record.A;
+import de.measite.minidns.record.AAAA;
+import de.measite.minidns.record.Data;
+import de.measite.minidns.record.SRV;
+import de.measite.minidns.util.NameUtil;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xmpp.jid.Jid;
public class DNSHelper {
@@ -38,17 +38,14 @@ public class DNSHelper {
public static Bundle getSRVRecord(final Jid jid) throws IOException {
final String host = jid.getDomainpart();
String dns[] = client.findDNS();
-
- if (dns != null) {
- for (String dnsserver : dns) {
- InetAddress ip = InetAddress.getByName(dnsserver);
- Bundle b = queryDNS(host, ip);
- if (b.containsKey("values")) {
- return b;
- }
+ for (int i = 0; i < dns.length; ++i) {
+ InetAddress ip = InetAddress.getByName(dns[i]);
+ Bundle b = queryDNS(host, ip);
+ if (b.containsKey("values") || i == dns.length - 1) {
+ return b;
}
}
- return queryDNS(host, InetAddress.getByName("8.8.8.8"));
+ return null;
}
public static Bundle queryDNS(String host, InetAddress dnsServer) {
@@ -132,7 +129,6 @@ public class DNSHelper {
} catch (SocketTimeoutException e) {
bundle.putString("error", "timeout");
} catch (Exception e) {
- e.printStackTrace();
bundle.putString("error", "unhandled");
}
return bundle;
diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
index 0ad57fe2..4e3ec236 100644
--- a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
+++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.utils;
+import android.content.Context;
+
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
@@ -8,8 +10,6 @@ import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
-import android.content.Context;
-
public class ExceptionHandler implements UncaughtExceptionHandler {
private UncaughtExceptionHandler defaultHandler;
diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
index ee3ea3e1..0f182847 100644
--- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
@@ -1,5 +1,17 @@
package eu.siacs.conversations.utils;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.preference.PreferenceManager;
+import android.text.format.DateUtils;
+import android.util.Log;
+
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
@@ -15,18 +27,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.content.DialogInterface.OnClickListener;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.preference.PreferenceManager;
-import android.text.format.DateUtils;
-import android.util.Log;
-
public class ExceptionHelper {
public static void init(Context context) {
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {
diff --git a/src/main/java/eu/siacs/conversations/utils/FileUtils.java b/src/main/java/eu/siacs/conversations/utils/FileUtils.java
new file mode 100644
index 00000000..a13300d4
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/FileUtils.java
@@ -0,0 +1,144 @@
+package eu.siacs.conversations.utils;
+
+import android.annotation.SuppressLint;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+
+public class FileUtils {
+
+ /**
+ * Get a file path from a Uri. This will get the the path for Storage Access
+ * Framework Documents, as well as the _data field for the MediaStore and
+ * other file-based ContentProviders.
+ *
+ * @param context The context.
+ * @param uri The Uri to query.
+ * @author paulburke
+ */
+ @SuppressLint("NewApi")
+ public static String getPath(final Context context, final Uri uri) {
+
+ final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+
+ // DocumentProvider
+ if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+ // ExternalStorageProvider
+ if (isExternalStorageDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ if ("primary".equalsIgnoreCase(type)) {
+ return Environment.getExternalStorageDirectory() + "/" + split[1];
+ }
+
+ // TODO handle non-primary volumes
+ }
+ // DownloadsProvider
+ else if (isDownloadsDocument(uri)) {
+
+ final String id = DocumentsContract.getDocumentId(uri);
+ final Uri contentUri = ContentUris.withAppendedId(
+ Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+
+ return getDataColumn(context, contentUri, null, null);
+ }
+ // MediaProvider
+ else if (isMediaDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ Uri contentUri = null;
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if ("video".equals(type)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else if ("audio".equals(type)) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[]{
+ split[1]
+ };
+
+ return getDataColumn(context, contentUri, selection, selectionArgs);
+ }
+ }
+ // MediaStore (and general)
+ else if ("content".equalsIgnoreCase(uri.getScheme())) {
+ return getDataColumn(context, uri, null, null);
+ }
+ // File
+ else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return uri.getPath();
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the value of the data column for this Uri. This is useful for
+ * MediaStore Uris, and other file-based ContentProviders.
+ *
+ * @param context The context.
+ * @param uri The Uri to query.
+ * @param selection (Optional) Filter used in the query.
+ * @param selectionArgs (Optional) Selection arguments used in the query.
+ * @return The value of the _data column, which is typically a file path.
+ */
+ public static String getDataColumn(Context context, Uri uri, String selection,
+ String[] selectionArgs) {
+
+ Cursor cursor = null;
+ final String column = "_data";
+ final String[] projection = {
+ column
+ };
+
+ try {
+ cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+ null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int column_index = cursor.getColumnIndexOrThrow(column);
+ return cursor.getString(column_index);
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ return null;
+ }
+
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is ExternalStorageProvider.
+ */
+ public static boolean isExternalStorageDocument(Uri uri) {
+ return "com.android.externalstorage.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is DownloadsProvider.
+ */
+ public static boolean isDownloadsDocument(Uri uri) {
+ return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is MediaProvider.
+ */
+ public static boolean isMediaDocument(Uri uri) {
+ return "com.android.providers.media.documents".equals(uri.getAuthority());
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
index a9e89d1b..d4544424 100644
--- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
+++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
@@ -458,7 +458,7 @@ public final class MimeUtils {
if (extension == null || extension.isEmpty()) {
return null;
}
- return extensionToMimeTypeMap.get(extension);
+ return extensionToMimeTypeMap.get(extension.toLowerCase());
}
/**
* Returns true if the given extension has a registered MIME type.
diff --git a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
index 9a689768..f18a4ed8 100644
--- a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
+++ b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
@@ -1,9 +1,9 @@
package eu.siacs.conversations.utils;
-import java.util.List;
-
import android.os.Bundle;
+import java.util.List;
+
public interface OnPhoneContactsLoadedListener {
public void onPhoneContactsLoaded(List<Bundle> phoneContacts);
}
diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
index 99e8ebb8..a37f60a0 100644
--- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
@@ -1,9 +1,5 @@
package eu.siacs.conversations.utils;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.RejectedExecutionException;
-
import android.content.Context;
import android.content.CursorLoader;
import android.content.Loader;
@@ -15,6 +11,9 @@ import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Profile;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
public class PhoneHelper {
public static void loadPhoneContacts(Context context,final List<Bundle> phoneContacts, final OnPhoneContactsLoadedListener listener) {
@@ -98,7 +97,7 @@ public class PhoneHelper {
if (packageName != null) {
try {
return context.getPackageManager().getPackageInfo(packageName, 0).versionName;
- } catch (final PackageManager.NameNotFoundException e) {
+ } catch (final PackageManager.NameNotFoundException | RuntimeException e) {
return "unknown";
}
} else {
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
index 76f48037..44a38728 100644
--- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -1,17 +1,15 @@
package eu.siacs.conversations.utils;
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.Pair;
+
import java.util.ArrayList;
import java.util.Arrays;
-import java.net.URLConnection;
import java.util.Calendar;
import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.regex.Pattern;
-import java.util.regex.Matcher;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ -22,14 +20,6 @@ import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.xmpp.jid.Jid;
-import android.content.Context;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-import android.text.Spannable.Factory;
-import android.text.style.ImageSpan;
-import android.text.Spannable;
-import android.util.Pair;
-
public class UIHelper {
private static String BLACK_HEART_SUIT = "\u2665";
diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java
index 32657c66..7b4937b2 100644
--- a/src/main/java/eu/siacs/conversations/xml/Element.java
+++ b/src/main/java/eu/siacs/conversations/xml/Element.java
@@ -21,6 +21,11 @@ public class Element {
this.name = name;
}
+ public Element(String name, String xmlns) {
+ this.name = name;
+ this.setAttribute("xmlns", xmlns);
+ }
+
public Element addChild(Element child) {
this.content = null;
children.add(child);
@@ -64,10 +69,9 @@ public class Element {
public Element findChild(String name, String xmlns) {
for (Element child : this.children) {
- if (child.getName().equals(name)
- && (child.getAttribute("xmlns").equals(xmlns))) {
+ if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) {
return child;
- }
+ }
}
return null;
}
diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java
index 52d3d46a..aeaaa593 100644
--- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java
+++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java
@@ -1,18 +1,18 @@
package eu.siacs.conversations.xml;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
-import eu.siacs.conversations.Config;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.util.Log;
-import android.util.Xml;
+import eu.siacs.conversations.Config;
public class XmlReader {
private XmlPullParser parser;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java b/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java
new file mode 100644
index 00000000..65ae133d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/OnKeyStatusUpdated.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.xmpp;
+
+public interface OnKeyStatusUpdated {
+ public void onKeyStatusUpdated();
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index 35c89b45..604325be 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -1,13 +1,10 @@
package eu.siacs.conversations.xmpp;
-import android.content.Context;
-import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
-import android.preference.PreferenceManager;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
@@ -26,7 +23,6 @@ import java.net.IDN;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
-import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
@@ -35,9 +31,9 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
+import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
import java.util.Map.Entry;
import javax.net.ssl.HostnameVerifier;
@@ -81,21 +77,20 @@ public class XmppConnection implements Runnable {
private static final int PACKET_IQ = 0;
private static final int PACKET_MESSAGE = 1;
private static final int PACKET_PRESENCE = 2;
- private final Context applicationContext;
protected Account account;
private final WakeLock wakeLock;
private Socket socket;
private XmlReader tagReader;
private TagWriter tagWriter;
private final Features features = new Features(this);
- private boolean shouldBind = true;
+ private boolean needsBinding = true;
private boolean shouldAuthenticate = true;
private Element streamFeatures;
private final HashMap<Jid, Info> disco = new HashMap<>();
private String streamId = null;
private int smVersion = 3;
- private final SparseArray<String> messageReceipts = new SparseArray<>();
+ private final SparseArray<String> mStanzaReceipts = new SparseArray<>();
private int stanzasReceived = 0;
private int stanzasSent = 0;
@@ -104,7 +99,7 @@ public class XmppConnection implements Runnable {
private long lastConnect = 0;
private long lastSessionStarted = 0;
private int attempt = 0;
- private final Map<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>();
+ private final Hashtable<String, Pair<IqPacket, OnIqPacketReceived>> packetCallbacks = new Hashtable<>();
private OnPresencePacketReceived presenceListener = null;
private OnJinglePacketReceived jingleListener = null;
private OnIqPacketReceived unregisteredIqListener = null;
@@ -123,7 +118,6 @@ public class XmppConnection implements Runnable {
PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
tagWriter = new TagWriter();
mXmppConnectionService = service;
- applicationContext = service.getApplicationContext();
}
protected void changeStatus(final Account.State nextStatus) {
@@ -151,10 +145,9 @@ public class XmppConnection implements Runnable {
lastPingSent = SystemClock.elapsedRealtime();
this.attempt++;
try {
- shouldAuthenticate = shouldBind = !account.isOptionSet(Account.OPTION_REGISTER);
+ shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER);
tagReader = new XmlReader(wakeLock);
tagWriter = new TagWriter();
- packetCallbacks.clear();
this.changeStatus(Account.State.CONNECTING);
if (DNSHelper.isIp(account.getServer().toString())) {
socket = new Socket();
@@ -165,6 +158,9 @@ public class XmppConnection implements Runnable {
}
} else {
final Bundle result = DNSHelper.getSRVRecord(account.getServer());
+ if (result == null) {
+ throw new IOException("unhandled exception in DNS resolver");
+ }
final ArrayList<Parcelable> values = result.getParcelableArrayList("values");
if ("timeout".equals(result.getString("error"))) {
throw new IOException("timeout in dns");
@@ -198,10 +194,7 @@ public class XmppConnection implements Runnable {
socket = new Socket();
socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
socketError = false;
- } catch (final UnknownHostException e) {
- Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
- i++;
- } catch (final IOException e) {
+ } catch (final Throwable e) {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage());
i++;
}
@@ -338,23 +331,24 @@ public class XmppConnection implements Runnable {
+ ": session resumed with lost packages");
stanzasSent = serverCount;
} else {
- Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
- + ": session resumed");
+ Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": session resumed");
}
- if (acknowledgedListener != null) {
- for (int i = 0; i < messageReceipts.size(); ++i) {
- if (serverCount >= messageReceipts.keyAt(i)) {
- acknowledgedListener.onMessageAcknowledged(
- account, messageReceipts.valueAt(i));
- }
+ acknowledgeStanzaUpTo(serverCount);
+ ArrayList<IqPacket> failedIqPackets = new ArrayList<>();
+ for(int i = 0; i < this.mStanzaReceipts.size(); ++i) {
+ String id = mStanzaReceipts.valueAt(i);
+ Pair<IqPacket,OnIqPacketReceived> pair = id == null ? null : this.packetCallbacks.get(id);
+ if (pair != null) {
+ failedIqPackets.add(pair.first);
}
}
- messageReceipts.clear();
+ mStanzaReceipts.clear();
+ Log.d(Config.LOGTAG,"resending "+failedIqPackets.size()+" iq stanza");
+ for(IqPacket packet : failedIqPackets) {
+ sendUnmodifiedIqPacket(packet,null);
+ }
} catch (final NumberFormatException ignored) {
}
- sendServiceDiscoveryInfo(account.getServer());
- sendServiceDiscoveryInfo(account.getJid().toBareJid());
- sendServiceDiscoveryItems(account.getServer());
sendInitialPing();
} else if (nextTag.isStart("r")) {
tagReader.readElement(nextTag);
@@ -368,17 +362,7 @@ public class XmppConnection implements Runnable {
lastPacketReceived = SystemClock.elapsedRealtime();
try {
final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
- if (Config.EXTENDED_SM_LOGGING) {
- Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + serverSequence);
- }
- final String msgId = this.messageReceipts.get(serverSequence);
- if (msgId != null) {
- if (this.acknowledgedListener != null) {
- this.acknowledgedListener.onMessageAcknowledged(
- account, msgId);
- }
- this.messageReceipts.remove(serverSequence);
- }
+ acknowledgeStanzaUpTo(serverSequence);
} catch (NumberFormatException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server send ack without sequence number");
}
@@ -406,6 +390,22 @@ public class XmppConnection implements Runnable {
}
}
+ private void acknowledgeStanzaUpTo(int serverCount) {
+ for (int i = 0; i < mStanzaReceipts.size(); ++i) {
+ if (serverCount >= mStanzaReceipts.keyAt(i)) {
+ if (Config.EXTENDED_SM_LOGGING) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + mStanzaReceipts.keyAt(i));
+ }
+ String id = mStanzaReceipts.valueAt(i);
+ if (acknowledgedListener != null) {
+ acknowledgedListener.onMessageAcknowledged(account, id);
+ }
+ mStanzaReceipts.removeAt(i);
+ i--;
+ }
+ }
+ }
+
private void sendInitialPing() {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping");
final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
@@ -414,9 +414,12 @@ public class XmppConnection implements Runnable {
this.sendIqPacket(iq, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
- Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
- + ": online with resource " + account.getResource());
- changeStatus(Account.State.ONLINE);
+ if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource());
+ changeStatus(Account.State.ONLINE);
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": initial ping failed");
+ }
}
});
}
@@ -481,26 +484,28 @@ public class XmppConnection implements Runnable {
this.jingleListener.onJinglePacketReceived(account,(JinglePacket) packet);
}
} else {
- if (packetCallbacks.containsKey(packet.getId())) {
- final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
- // Packets to the server should have responses from the server
- if (packetCallbackDuple.first.toServer(account)) {
- if (packet.fromServer(account)) {
- packetCallbackDuple.second.onIqPacketReceived(account, packet);
- packetCallbacks.remove(packet.getId());
- } else {
- Log.e(Config.LOGTAG,account.getJid().toBareJid().toString()+": ignoring spoofed iq packet");
- }
- } else {
- if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
- packetCallbackDuple.second.onIqPacketReceived(account, packet);
- packetCallbacks.remove(packet.getId());
+ synchronized (this.packetCallbacks) {
+ if (packetCallbacks.containsKey(packet.getId())) {
+ final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
+ // Packets to the server should have responses from the server
+ if (packetCallbackDuple.first.toServer(account)) {
+ if (packet.fromServer(account)) {
+ packetCallbackDuple.second.onIqPacketReceived(account, packet);
+ packetCallbacks.remove(packet.getId());
+ } else {
+ Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
+ }
} else {
- Log.e(Config.LOGTAG,account.getJid().toBareJid().toString()+": ignoring spoofed iq packet");
+ if (packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
+ packetCallbackDuple.second.onIqPacketReceived(account, packet);
+ packetCallbacks.remove(packet.getId());
+ } else {
+ Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet");
+ }
}
+ } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
+ this.unregisteredIqListener.onIqPacketReceived(account, packet);
}
- } else if (packet.getType() == IqPacket.TYPE.GET|| packet.getType() == IqPacket.TYPE.SET) {
- this.unregisteredIqListener.onIqPacketReceived(account, packet);
}
}
}
@@ -521,14 +526,6 @@ public class XmppConnection implements Runnable {
tagWriter.writeTag(startTLS);
}
- private SharedPreferences getPreferences() {
- return PreferenceManager.getDefaultSharedPreferences(applicationContext);
- }
-
- private boolean enableLegacySSL() {
- return getPreferences().getBoolean("enable_legacy_ssl", false);
- }
-
private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
tagReader.readTag();
try {
@@ -629,19 +626,18 @@ public class XmppConnection implements Runnable {
} else {
throw new IncompatibleServerException();
}
- } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
- + smVersion)
- && streamId != null) {
+ } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) {
if (Config.EXTENDED_SM_LOGGING) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": resuming after stanza #"+stanzasReceived);
}
final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion);
this.tagWriter.writeStanzaAsync(resume);
- } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
- sendBindRequest();
- } else {
- disconnect(true);
- changeStatus(Account.State.INCOMPATIBLE_SERVER);
+ } else if (needsBinding) {
+ if (this.streamFeatures.hasChild("bind")) {
+ sendBindRequest();
+ } else {
+ throw new IncompatibleServerException();
+ }
}
}
@@ -662,8 +658,8 @@ public class XmppConnection implements Runnable {
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
- final Element instructions = packet.query().findChild("instructions");
- if (packet.query().hasChild("username")
+ if (packet.getType() == IqPacket.TYPE.RESULT
+ && packet.query().hasChild("username")
&& (packet.query().hasChild("password"))) {
final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
final Element username = new Element("username").setContent(account.getUsername());
@@ -690,6 +686,7 @@ public class XmppConnection implements Runnable {
}
});
} else {
+ final Element instructions = packet.query().findChild("instructions");
changeStatus(Account.State.REGISTRATION_FAILED);
disconnect(true);
Log.d(Config.LOGTAG, account.getJid().toBareJid()
@@ -707,6 +704,8 @@ public class XmppConnection implements Runnable {
} catch (final InterruptedException ignored) {
}
}
+ needsBinding = false;
+ clearIqCallbacks();
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
.addChild("resource").setContent(account.getResource());
@@ -728,24 +727,48 @@ public class XmppConnection implements Runnable {
sendPostBindInitialization();
}
} else {
+ Log.d(Config.LOGTAG,account.getJid()+": disconnecting because of bind failure");
disconnect(true);
}
} else {
+ Log.d(Config.LOGTAG,account.getJid()+": disconnecting because of bind failure");
disconnect(true);
}
}
});
}
+ private void clearIqCallbacks() {
+ final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT);
+ final ArrayList<OnIqPacketReceived> callbacks = new ArrayList<>();
+ synchronized (this.packetCallbacks) {
+ if (this.packetCallbacks.size() == 0) {
+ return;
+ }
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": clearing "+this.packetCallbacks.size()+" iq callbacks");
+ final Iterator<Pair<IqPacket, OnIqPacketReceived>> iterator = this.packetCallbacks.values().iterator();
+ while (iterator.hasNext()) {
+ Pair<IqPacket, OnIqPacketReceived> entry = iterator.next();
+ callbacks.add(entry.second);
+ iterator.remove();
+ }
+ }
+ for(OnIqPacketReceived callback : callbacks) {
+ callback.onIqPacketReceived(account,failurePacket);
+ }
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": done clearing iq callbacks. "+this.packetCallbacks.size()+" left");
+ }
+
private void sendStartSession() {
final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
- startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session");
+ startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.RESULT) {
sendPostBindInitialization();
} else {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not init sessions");
disconnect(true);
}
}
@@ -763,7 +786,7 @@ public class XmppConnection implements Runnable {
final EnablePacket enable = new EnablePacket(smVersion);
tagWriter.writeStanzaAsync(enable);
stanzasSent = 0;
- messageReceipts.clear();
+ mStanzaReceipts.clear();
}
features.carbonsEnabled = false;
features.blockListRequested = false;
@@ -790,26 +813,29 @@ public class XmppConnection implements Runnable {
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
- final List<Element> elements = packet.query().getChildren();
- final Info info = new Info();
- for (final Element element : elements) {
- if (element.getName().equals("identity")) {
- String type = element.getAttribute("type");
- String category = element.getAttribute("category");
- if (type != null && category != null) {
- info.identities.add(new Pair<>(category,type));
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ final List<Element> elements = packet.query().getChildren();
+ final Info info = new Info();
+ for (final Element element : elements) {
+ if (element.getName().equals("identity")) {
+ String type = element.getAttribute("type");
+ String category = element.getAttribute("category");
+ if (type != null && category != null) {
+ info.identities.add(new Pair<>(category, type));
+ }
+ } else if (element.getName().equals("feature")) {
+ info.features.add(element.getAttribute("var"));
}
- } else if (element.getName().equals("feature")) {
- info.features.add(element.getAttribute("var"));
}
- }
- disco.put(jid, info);
-
- if (account.getServer().equals(jid)) {
- enableAdvancedStreamFeatures();
- for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
- listener.onAdvancedStreamFeaturesAvailable(account);
+ disco.put(jid, info);
+ if (account.getServer().equals(jid)) {
+ enableAdvancedStreamFeatures();
+ for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) {
+ listener.onAdvancedStreamFeaturesAvailable(account);
+ }
}
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not query disco info for "+jid.toString());
}
}
});
@@ -834,14 +860,18 @@ public class XmppConnection implements Runnable {
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
- final List<Element> elements = packet.query().getChildren();
- for (final Element element : elements) {
- if (element.getName().equals("item")) {
- final Jid jid = element.getAttributeAsJid("jid");
- if (jid != null && !jid.equals(account.getServer())) {
- sendServiceDiscoveryInfo(jid);
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ final List<Element> elements = packet.query().getChildren();
+ for (final Element element : elements) {
+ if (element.getName().equals("item")) {
+ final Jid jid = element.getAttributeAsJid("jid");
+ if (jid != null && !jid.equals(account.getServer())) {
+ sendServiceDiscoveryInfo(jid);
+ }
}
}
+ } else {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not query disco items of "+server);
}
}
});
@@ -875,6 +905,8 @@ public class XmppConnection implements Runnable {
Log.d(Config.LOGTAG,
account.getJid().toBareJid() + ": switching resource due to conflict ("
+ account.getResource() + ")");
+ } else if (streamError != null) {
+ Log.d(Config.LOGTAG,account.getJid().toBareJid()+": stream error "+streamError.toString());
}
}
@@ -895,7 +927,7 @@ public class XmppConnection implements Runnable {
public void sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
packet.setFrom(account.getJid());
- this.sendUnmodifiedIqPacket(packet,callback);
+ this.sendUnmodifiedIqPacket(packet, callback);
}
@@ -905,10 +937,9 @@ public class XmppConnection implements Runnable {
packet.setAttribute("id", id);
}
if (callback != null) {
- if (packet.getId() == null) {
- packet.setId(nextRandomId());
+ synchronized (this.packetCallbacks) {
+ packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
}
- packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
}
this.sendPacket(packet);
}
@@ -932,11 +963,11 @@ public class XmppConnection implements Runnable {
++stanzasSent;
}
tagWriter.writeStanzaAsync(packet);
- if (packet instanceof MessagePacket && packet.getId() != null && getFeatures().sm()) {
+ if ((packet instanceof MessagePacket || packet instanceof IqPacket) && packet.getId() != null && this.streamId != null) {
if (Config.EXTENDED_SM_LOGGING) {
- Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for message stanza #" + stanzasSent);
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for stanza #" + stanzasSent);
}
- this.messageReceipts.put(stanzasSent, packet.getId());
+ this.mStanzaReceipts.put(stanzasSent, packet.getId());
tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
}
}
@@ -1005,7 +1036,7 @@ public class XmppConnection implements Runnable {
if (tagWriter.isActive()) {
tagWriter.finish();
try {
- while (!tagWriter.finished()) {
+ while (!tagWriter.finished() && socket.isConnected()) {
Log.d(Config.LOGTAG, "not yet finished");
Thread.sleep(100);
}
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 2c56488d..42b14330 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -1,5 +1,13 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
@@ -8,17 +16,18 @@ import java.util.Locale;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.SystemClock;
-import android.util.Log;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
@@ -59,20 +68,24 @@ public class JingleConnection implements Transferable {
private String contentCreator;
private int mProgress = 0;
- private long mLastGuiRefresh = 0;
private boolean receivedCandidate = false;
private boolean sentCandidate = false;
private boolean acceptedAutomatically = false;
+ private XmppAxolotlMessage mXmppAxolotlMessage;
+
private JingleTransport transport = null;
+ private OutputStream mFileOutputStream;
+ private InputStream mFileInputStream;
+
private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.ERROR) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
fail();
}
}
@@ -84,15 +97,13 @@ public class JingleConnection implements Transferable {
public void onFileTransmitted(DownloadableFile file) {
if (responder.equals(account.getJid())) {
sendSuccess();
+ mXmppConnectionService.getFileBackend().updateFileParams(message);
+ mXmppConnectionService.databaseBackend.createMessage(message);
+ mXmppConnectionService.markMessage(message,Message.STATUS_RECEIVED);
if (acceptedAutomatically) {
message.markUnread();
- JingleConnection.this.mXmppConnectionService
- .getNotificationService().push(message);
+ JingleConnection.this.mXmppConnectionService.getNotificationService().push(message);
}
- mXmppConnectionService.getFileBackend().updateFileParams(message);
- mXmppConnectionService.databaseBackend.createMessage(message);
- mXmppConnectionService.markMessage(message,
- Message.STATUS_RECEIVED);
} else {
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
file.delete();
@@ -113,6 +124,14 @@ public class JingleConnection implements Transferable {
}
};
+ public InputStream getFileInputStream() {
+ return this.mFileInputStream;
+ }
+
+ public OutputStream getFileOutputStream() {
+ return this.mFileOutputStream;
+ }
+
private OnProxyActivated onProxyActivated = new OnProxyActivated() {
@Override
@@ -194,7 +213,22 @@ public class JingleConnection implements Transferable {
mXmppConnectionService.sendIqPacket(account,response,null);
}
- public void init(Message message) {
+ public void init(final Message message) {
+ if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ Conversation conversation = message.getConversation();
+ conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation.getContact(), new OnMessageCreatedCallback() {
+ @Override
+ public void run(XmppAxolotlMessage xmppAxolotlMessage) {
+ init(message, xmppAxolotlMessage);
+ }
+ });
+ } else {
+ init(message, null);
+ }
+ }
+
+ private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
+ this.mXmppAxolotlMessage = xmppAxolotlMessage;
this.contentCreator = "initiator";
this.contentName = this.mJingleConnectionManager.nextRandomId();
this.message = message;
@@ -238,8 +272,7 @@ public class JingleConnection implements Transferable {
});
mergeCandidate(candidate);
} else {
- Log.d(Config.LOGTAG,
- "no primary candidate of our own was found");
+ Log.d(Config.LOGTAG, "no primary candidate of our own was found");
sendInitRequest();
}
}
@@ -267,13 +300,16 @@ public class JingleConnection implements Transferable {
this.contentCreator = content.getAttribute("creator");
this.contentName = content.getAttribute("name");
this.transportId = content.getTransportId();
- this.mergeCandidates(JingleCandidate.parse(content.socks5transport()
- .getChildren()));
+ this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
this.fileOffer = packet.getJingleContent().getFileOffer();
mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
if (fileOffer != null) {
+ Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
+ if (encrypted != null) {
+ this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid());
+ }
Element fileSize = fileOffer.findChild("size");
Element fileNameElement = fileOffer.findChild("name");
if (fileNameElement != null) {
@@ -333,22 +369,36 @@ public class JingleConnection implements Transferable {
+ " allowed size:"
+ this.mJingleConnectionManager
.getAutoAcceptFileSize());
- this.mXmppConnectionService.getNotificationService()
- .push(message);
+ this.mXmppConnectionService.getNotificationService().push(message);
}
- this.file = this.mXmppConnectionService.getFileBackend()
- .getFile(message, false);
- if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
+ if (mXmppAxolotlMessage != null) {
+ XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage);
+ if (transportMessage != null) {
+ message.setEncryption(Message.ENCRYPTION_AXOLOTL);
+ this.file.setKey(transportMessage.getKey());
+ this.file.setIv(transportMessage.getIv());
+ message.setAxolotlFingerprint(transportMessage.getFingerprint());
+ } else {
+ Log.d(Config.LOGTAG,"could not process KeyTransportMessage");
+ }
+ } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
byte[] key = conversation.getSymmetricKey();
if (key == null) {
this.sendCancel();
this.fail();
return;
} else {
- this.file.setKey(key);
+ this.file.setKeyAndIv(key);
}
}
- this.file.setExpectedSize(size);
+ this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file,message.getEncryption() == Message.ENCRYPTION_AXOLOTL);
+ if (message.getEncryption() == Message.ENCRYPTION_OTR && Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE) {
+ this.file.setExpectedSize((size / 16 + 1) * 16);
+ } else {
+ this.file.setExpectedSize(size);
+ }
+ Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
} else {
this.sendCancel();
this.fail();
@@ -364,19 +414,35 @@ public class JingleConnection implements Transferable {
Content content = new Content(this.contentCreator, this.contentName);
if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
content.setTransportId(this.transportId);
- this.file = this.mXmppConnectionService.getFileBackend().getFile(
- message, false);
- if (message.getEncryption() == Message.ENCRYPTION_OTR) {
- Conversation conversation = this.message.getConversation();
- if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
- Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not set symmetric key");
- cancel();
+ this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
+ Pair<InputStream,Integer> pair;
+ try {
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ Conversation conversation = this.message.getConversation();
+ if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
+ Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key");
+ cancel();
+ }
+ this.file.setKeyAndIv(conversation.getSymmetricKey());
+ pair = AbstractConnectionManager.createInputStream(this.file, false);
+ this.file.setExpectedSize(pair.second);
+ content.setFileOffer(this.file, true);
+ } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ this.file.setKey(mXmppAxolotlMessage.getInnerKey());
+ this.file.setIv(mXmppAxolotlMessage.getIV());
+ pair = AbstractConnectionManager.createInputStream(this.file, true);
+ this.file.setExpectedSize(pair.second);
+ content.setFileOffer(this.file, false).addChild(mXmppAxolotlMessage.toElement());
+ } else {
+ pair = AbstractConnectionManager.createInputStream(this.file, false);
+ this.file.setExpectedSize(pair.second);
+ content.setFileOffer(this.file, false);
}
- content.setFileOffer(this.file, true);
- this.file.setKey(conversation.getSymmetricKey());
- } else {
- content.setFileOffer(this.file, false);
+ } catch (FileNotFoundException e) {
+ cancel();
+ return;
}
+ this.mFileInputStream = pair.first;
this.transportId = this.mJingleConnectionManager.nextRandomId();
content.setTransportId(this.transportId);
content.socks5transport().setChildren(getCandidatesAsElements());
@@ -385,7 +451,7 @@ public class JingleConnection implements Transferable {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
- if (packet.getType() != IqPacket.TYPE.ERROR) {
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer");
mJingleStatus = JINGLE_STATUS_INITIATED;
mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
@@ -570,12 +636,11 @@ public class JingleConnection implements Transferable {
@Override
public void onIqPacketReceived(Account account,
IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.ERROR) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
onProxyActivated.failed();
} else {
onProxyActivated.success();
- sendProxyActivated(connection
- .getCandidate().getCid());
+ sendProxyActivated(connection.getCandidate().getCid());
}
}
});
@@ -748,6 +813,8 @@ public class JingleConnection implements Transferable {
if (this.transport != null && this.transport instanceof JingleInbandTransport) {
this.transport.disconnect();
}
+ FileBackend.close(mFileInputStream);
+ FileBackend.close(mFileOutputStream);
if (this.message != null) {
if (this.responder.equals(account.getJid())) {
this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
@@ -901,10 +968,7 @@ public class JingleConnection implements Transferable {
public void updateProgress(int i) {
this.mProgress = i;
- if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
- this.mLastGuiRefresh = SystemClock.elapsedRealtime();
- mXmppConnectionService.updateConversationUi();
- }
+ mXmppConnectionService.updateConversationUi();
}
interface OnProxyActivated {
@@ -956,4 +1020,8 @@ public class JingleConnection implements Transferable {
public int getProgress() {
return this.mProgress;
}
+
+ public AbstractConnectionManager getConnectionManager() {
+ return this.mJingleConnectionManager;
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
index cadf9df3..2c888d0f 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -1,16 +1,19 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.annotation.SuppressLint;
+import android.util.Log;
+
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
-import android.annotation.SuppressLint;
-import android.util.Log;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Xmlns;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
index 9a02ee7a..85280c5c 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java
@@ -1,5 +1,8 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.util.Base64;
+import android.util.Log;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -7,9 +10,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
-import android.util.Base64;
-import android.util.Log;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
@@ -74,7 +74,7 @@ public class JingleInbandTransport extends JingleTransport {
@Override
public void onIqPacketReceived(Account account,
IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.ERROR) {
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
callback.failed();
} else {
callback.established();
@@ -93,7 +93,7 @@ public class JingleInbandTransport extends JingleTransport {
digest.reset();
file.getParentFile().mkdirs();
file.createNewFile();
- this.fileOutputStream = file.createOutputStream();
+ this.fileOutputStream = connection.getFileOutputStream();
if (this.fileOutputStream == null) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream");
callback.onFileTransferAborted();
@@ -112,15 +112,11 @@ public class JingleInbandTransport extends JingleTransport {
this.onFileTransmissionStatusChanged = callback;
this.file = file;
try {
- if (this.file.getKey() != null) {
- this.remainingSize = (this.file.getSize() / 16 + 1) * 16;
- } else {
- this.remainingSize = this.file.getSize();
- }
+ this.remainingSize = this.file.getExpectedSize();
this.fileSize = this.remainingSize;
this.digest = MessageDigest.getInstance("SHA-1");
this.digest.reset();
- fileInputStream = this.file.createInputStream();
+ fileInputStream = connection.getFileInputStream();
if (fileInputStream == null) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream");
callback.onFileTransferAborted();
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
index 8d74f44e..556395ae 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
@@ -1,5 +1,6 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.os.PowerManager;
import android.util.Log;
import java.io.FileNotFoundException;
@@ -96,23 +97,24 @@ public class JingleSocks5Transport extends JingleTransport {
}
- public void send(final DownloadableFile file,
- final OnFileTransmissionStatusChanged callback) {
+ public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
new Thread(new Runnable() {
@Override
public void run() {
InputStream fileInputStream = null;
+ final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_"+connection.getSessionId());
try {
+ wakeLock.acquire();
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset();
- fileInputStream = file.createInputStream();
+ fileInputStream = connection.getFileInputStream();
if (fileInputStream == null) {
Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream");
callback.onFileTransferAborted();
return;
}
- long size = file.getSize();
+ long size = file.getExpectedSize();
long transmitted = 0;
int count;
byte[] buffer = new byte[8192];
@@ -138,6 +140,7 @@ public class JingleSocks5Transport extends JingleTransport {
callback.onFileTransferAborted();
} finally {
FileBackend.close(fileInputStream);
+ wakeLock.release();
}
}
}).start();
@@ -150,14 +153,16 @@ public class JingleSocks5Transport extends JingleTransport {
@Override
public void run() {
OutputStream fileOutputStream = null;
+ final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_"+connection.getSessionId());
try {
+ wakeLock.acquire();
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset();
inputStream.skip(45);
socket.setSoTimeout(30000);
file.getParentFile().mkdirs();
file.createNewFile();
- fileOutputStream = file.createOutputStream();
+ fileOutputStream = connection.getFileOutputStream();
if (fileOutputStream == null) {
callback.onFileTransferAborted();
Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream");
@@ -166,7 +171,7 @@ public class JingleSocks5Transport extends JingleTransport {
double size = file.getExpectedSize();
long remainingSize = file.getExpectedSize();
byte[] buffer = new byte[8192];
- int count = buffer.length;
+ int count;
while (remainingSize > 0) {
count = inputStream.read(buffer);
if (count == -1) {
@@ -194,7 +199,9 @@ public class JingleSocks5Transport extends JingleTransport {
Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
callback.onFileTransferAborted();
} finally {
+ wakeLock.release();
FileBackend.close(fileOutputStream);
+ FileBackend.close(inputStream);
}
}
}).start();
@@ -209,27 +216,9 @@ public class JingleSocks5Transport extends JingleTransport {
}
public void disconnect() {
- if (this.outputStream != null) {
- try {
- this.outputStream.close();
- } catch (IOException e) {
-
- }
- }
- if (this.inputStream != null) {
- try {
- this.inputStream.close();
- } catch (IOException e) {
-
- }
- }
- if (this.socket != null) {
- try {
- this.socket.close();
- } catch (IOException e) {
-
- }
- }
+ FileBackend.close(inputStream);
+ FileBackend.close(outputStream);
+ FileBackend.close(socket);
}
public boolean isEstablished() {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
index e832d3f5..b3211158 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
@@ -1,5 +1,31 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.util.Log;
+import android.util.Pair;
+
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.modes.AEADBlockCipher;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.DownloadableFile;
public abstract class JingleTransport {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
index bcadbe77..f752cc5d 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
@@ -25,17 +25,18 @@ public class Content extends Element {
this.transportId = sid;
}
- public void setFileOffer(DownloadableFile actualFile, boolean otr) {
+ public Element setFileOffer(DownloadableFile actualFile, boolean otr) {
Element description = this.addChild("description",
"urn:xmpp:jingle:apps:file-transfer:3");
Element offer = description.addChild("offer");
Element file = offer.addChild("file");
- file.addChild("size").setContent(Long.toString(actualFile.getSize()));
+ file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize()));
if (otr) {
file.addChild("name").setContent(actualFile.getName() + ".otr");
} else {
file.addChild("name").setContent(actualFile.getName());
}
+ return file;
}
public Element getFileOffer() {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
index 74da6a9b..38bb5c8f 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java
@@ -1,10 +1,10 @@
package eu.siacs.conversations.xmpp.pep;
+import android.util.Base64;
+
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
-import android.util.Base64;
-
public class Avatar {
public enum Origin { PEP, VCARD };
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
index 7b36fc49..49b27408 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java
@@ -4,12 +4,13 @@ import eu.siacs.conversations.xml.Element;
public class IqPacket extends AbstractStanza {
- public static enum TYPE {
+ public enum TYPE {
ERROR,
SET,
RESULT,
GET,
- INVALID
+ INVALID,
+ TIMEOUT
}
public IqPacket(final TYPE type) {
@@ -39,6 +40,9 @@ public class IqPacket extends AbstractStanza {
public TYPE getType() {
final String type = getAttribute("type");
+ if (type == null) {
+ return TYPE.INVALID;
+ }
switch (type) {
case "error":
return TYPE.ERROR;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
index e32811af..6b690912 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java
@@ -2,8 +2,6 @@ package eu.siacs.conversations.xmpp.stanzas;
import android.util.Pair;
-import java.text.ParseException;
-
import eu.siacs.conversations.parser.AbstractParser;
import eu.siacs.conversations.xml.Element;
@@ -29,6 +27,11 @@ public class MessagePacket extends AbstractStanza {
this.children.add(0, body);
}
+ public void setAxolotlMessage(Element axolotlMessage) {
+ this.children.remove(findChild("body"));
+ this.children.add(0, axolotlMessage);
+ }
+
public void setType(int type) {
switch (type) {
case TYPE_CHAT: