diff --git a/build.gradle b/build.gradle
index 4b858ae38..b6ce8ff67 100644
--- a/build.gradle
+++ b/build.gradle
@@ -36,6 +36,7 @@ configurations {
 }
 
 dependencies {
+    implementation 'org.jitsi:org.otr4j:0.23'
     implementation 'com.github.webrtc-sdk:android:93.4577.01'
     implementation project(':libs:android-transcoder')
     playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') {
diff --git a/src/git/java/eu/siacs/conversations/services/EmojiService.java b/src/git/java/eu/siacs/conversations/services/EmojiService.java
deleted file mode 100644
index e12a0d1a0..000000000
--- a/src/git/java/eu/siacs/conversations/services/EmojiService.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package eu.siacs.conversations.services;
-
-import android.content.Context;
-import android.os.Build;
-
-import androidx.emoji.bundled.BundledEmojiCompatConfig;
-import androidx.emoji.text.EmojiCompat;
-
-public class EmojiService {
-
-    private final Context context;
-
-    public EmojiService(Context context) {
-        this.context = context;
-    }
-
-    public void init(boolean useBundledEmoji) {
-        BundledEmojiCompatConfig config = new BundledEmojiCompatConfig(context);
-        //On recent Androids we assume to have the latest emojis
-        //there are some annoying bugs with emoji compat that make it a safer choice not to use it when possible
-        // a) the text preview has annoying glitches when the cut of text contains emojis (the emoji will be half visible)
-        // b) can trigger a hardware rendering bug https://issuetracker.google.com/issues/67102093
-        config.setReplaceAll(useBundledEmoji && Build.VERSION.SDK_INT < Build.VERSION_CODES.O);
-        EmojiCompat.init(config);
-    }
-}
\ No newline at end of file
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 43111b4e2..54a3c6d36 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -284,6 +284,10 @@
         <activity
             android:name=".ui.PublishGroupChatProfilePictureActivity"
             android:label="@string/group_chat_avatar" />
+        <activity
+            android:name=".ui.VerifyOTRActivity"
+            android:label="@string/verify_otr"
+            android:windowSoftInputMode="stateHidden" />
         <activity
             android:name=".ui.ShareWithActivity"
             android:label="@string/app_name"
diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrService.java b/src/main/java/eu/siacs/conversations/crypto/OtrService.java
new file mode 100644
index 000000000..61b86d259
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/OtrService.java
@@ -0,0 +1,312 @@
+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;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.DSAPrivateKeySpec;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.generator.MessageGenerator;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.chatstate.ChatState;
+import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+
+public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
+
+    private Account account;
+    private OtrPolicy otrPolicy;
+    private KeyPair keyPair;
+    private XmppConnectionService mXmppConnectionService;
+
+    public OtrService(XmppConnectionService service, Account account) {
+        this.account = account;
+        this.otrPolicy = new OtrPolicyImpl();
+        this.otrPolicy.setAllowV1(false);
+        this.otrPolicy.setAllowV2(true);
+        this.otrPolicy.setAllowV3(true);
+        this.keyPair = loadKey(account.getKeys());
+        this.mXmppConnectionService = service;
+    }
+
+    private KeyPair loadKey(final JSONObject keys) {
+        if (keys == null) {
+            return null;
+        }
+        synchronized (keys) {
+            try {
+                BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
+                BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
+                BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
+                BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
+                BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
+                KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+                DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
+                DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
+                PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
+                PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
+                return new KeyPair(publicKey, privateKey);
+            } catch (JSONException e) {
+                return null;
+            } catch (NoSuchAlgorithmException e) {
+                return null;
+            } catch (InvalidKeySpecException e) {
+                return null;
+            }
+        }
+    }
+
+    private void saveKey() {
+        PublicKey publicKey = keyPair.getPublic();
+        PrivateKey privateKey = keyPair.getPrivate();
+        KeyFactory keyFactory;
+        try {
+            keyFactory = KeyFactory.getInstance("DSA");
+            DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(
+                    privateKey, DSAPrivateKeySpec.class);
+            DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey,
+                    DSAPublicKeySpec.class);
+            this.account.setKey("otr_x", privateKeySpec.getX().toString(16));
+            this.account.setKey("otr_g", privateKeySpec.getG().toString(16));
+            this.account.setKey("otr_p", privateKeySpec.getP().toString(16));
+            this.account.setKey("otr_q", privateKeySpec.getQ().toString(16));
+            this.account.setKey("otr_y", publicKeySpec.getY().toString(16));
+        } catch (final NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        } catch (final InvalidKeySpecException e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    @Override
+    public void askForSecret(SessionID id, InstanceTag instanceTag, String question) {
+        try {
+            final Jid jid = OtrJidHelper.fromSessionID(id);
+            Conversation conversation = this.mXmppConnectionService.find(this.account, jid);
+            if (conversation != null) {
+                conversation.smp().hint = question;
+                conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED;
+                mXmppConnectionService.updateConversationUi();
+            }
+        } catch (IllegalArgumentException e) {
+            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": smp in invalid session " + id.toString());
+        }
+    }
+
+    @Override
+    public void finishedSessionMessage(SessionID arg0, String arg1)
+            throws OtrException {
+
+    }
+
+    @Override
+    public String getFallbackMessage(SessionID arg0) {
+        return MessageGenerator.OTR_FALLBACK_MESSAGE;
+    }
+
+    @Override
+    public byte[] getLocalFingerprintRaw(SessionID arg0) {
+        try {
+            return getFingerprintRaw(getPublicKey());
+        } catch (OtrCryptoException e) {
+            return null;
+        }
+    }
+
+    public PublicKey getPublicKey() {
+        if (this.keyPair == null) {
+            return null;
+        }
+        return this.keyPair.getPublic();
+    }
+
+    @Override
+    public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException {
+        if (this.keyPair == null) {
+            KeyPairGenerator kg;
+            try {
+                kg = KeyPairGenerator.getInstance("DSA");
+                this.keyPair = kg.genKeyPair();
+                this.saveKey();
+                mXmppConnectionService.databaseBackend.updateAccount(account);
+            } catch (NoSuchAlgorithmException e) {
+                Log.d(Config.LOGTAG,
+                        "error generating key pair " + e.getMessage());
+            }
+        }
+        return this.keyPair;
+    }
+
+    @Override
+    public String getReplyForUnreadableMessage(SessionID arg0) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    @Override
+    public OtrPolicy getSessionPolicy(SessionID arg0) {
+        return otrPolicy;
+    }
+
+    @Override
+    public void injectMessage(SessionID session, String body)
+            throws OtrException {
+        MessagePacket packet = new MessagePacket();
+        packet.setFrom(account.getJid());
+        if (session.getUserID().isEmpty()) {
+            packet.setAttribute("to", session.getAccountID());
+        } else {
+            packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID());
+        }
+        packet.setBody(body);
+        MessageGenerator.addMessageHints(packet);
+        try {
+            Jid jid = OtrJidHelper.fromSessionID(session);
+            Conversation conversation = mXmppConnectionService.find(account, jid);
+            if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
+                if (mXmppConnectionService.sendChatStates()) {
+                    packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
+                }
+            }
+        } catch (final IllegalArgumentException ignored) {
+
+        }
+
+        packet.setType(MessagePacket.TYPE_CHAT);
+        packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0");
+        account.getXmppConnection().sendMessagePacket(packet);
+    }
+
+    @Override
+    public void messageFromAnotherInstanceReceived(SessionID session) {
+        sendOtrErrorMessage(session, "Message from another OTR-instance received");
+    }
+
+    @Override
+    public void multipleInstancesDetected(SessionID arg0) {
+        // TODO Auto-generated method stub
+
+    }
+
+    @Override
+    public void requireEncryptedMessage(SessionID arg0, String arg1)
+            throws OtrException {
+        // TODO Auto-generated method stub
+
+    }
+
+    @Override
+    public void showError(SessionID arg0, String arg1) throws OtrException {
+        Log.d(Config.LOGTAG, "show error");
+    }
+
+    @Override
+    public void smpAborted(SessionID id) throws OtrException {
+        setSmpStatus(id, Conversation.Smp.STATUS_NONE);
+    }
+
+    private void setSmpStatus(SessionID id, int status) {
+        try {
+            final Jid jid = OtrJidHelper.fromSessionID(id);
+            Conversation conversation = this.mXmppConnectionService.find(this.account, jid);
+            if (conversation != null) {
+                conversation.smp().status = status;
+                mXmppConnectionService.updateConversationUi();
+            }
+        } catch (final IllegalArgumentException ignored) {
+
+        }
+    }
+
+    @Override
+    public void smpError(SessionID id, int arg1, boolean arg2)
+            throws OtrException {
+        setSmpStatus(id, Conversation.Smp.STATUS_NONE);
+    }
+
+    @Override
+    public void unencryptedMessageReceived(SessionID arg0, String arg1)
+            throws OtrException {
+        throw new OtrException(new Exception("unencrypted message received"));
+    }
+
+    @Override
+    public void unreadableMessageReceived(SessionID session) throws OtrException {
+        Log.d(Config.LOGTAG, "unreadable message received");
+        sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message");
+    }
+
+    public void sendOtrErrorMessage(SessionID session, String errorText) {
+        try {
+            Jid jid = OtrJidHelper.fromSessionID(session);
+            Conversation conversation = mXmppConnectionService.find(account, jid);
+            String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId();
+            if (id != null) {
+                MessagePacket packet = mXmppConnectionService.getMessageGenerator()
+                        .generateOtrError(jid, id, errorText);
+                packet.setFrom(account.getJid());
+                mXmppConnectionService.sendMessagePacket(account, packet);
+                Log.d(Config.LOGTAG, packet.toString());
+                Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
+                        + ": unreadable OTR message in " + conversation.getName());
+            }
+        } catch (IllegalArgumentException e) {
+            return;
+        }
+    }
+
+    @Override
+    public void unverify(SessionID id, String arg1) {
+        setSmpStatus(id, Conversation.Smp.STATUS_FAILED);
+    }
+
+    @Override
+    public void verify(SessionID id, String fingerprint, boolean approved) {
+        Log.d(Config.LOGTAG, "OtrService.verify(" + id.toString() + "," + fingerprint + "," + String.valueOf(approved) + ")");
+        try {
+            final Jid jid = OtrJidHelper.fromSessionID(id);
+            Conversation conversation = this.mXmppConnectionService.find(this.account, jid);
+            if (conversation != null) {
+                if (approved) {
+                    conversation.getContact().addOtrFingerprint(fingerprint);
+                }
+                conversation.smp().hint = null;
+                conversation.smp().status = Conversation.Smp.STATUS_VERIFIED;
+                mXmppConnectionService.updateConversationUi();
+                mXmppConnectionService.syncRosterToDisk(conversation.getAccount());
+            }
+        } catch (final IllegalArgumentException ignored) {
+        }
+    }
+
+    @Override
+    public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) {
+        return null;
+    }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
index c471cabf3..582ed54dd 100644
--- a/src/main/java/eu/siacs/conversations/entities/Account.java
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -7,6 +7,13 @@ import android.util.Log;
 
 import com.google.common.base.Strings;
 
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+import java.security.PublicKey;
+import java.security.interfaces.DSAPublicKey;
+import java.util.Locale;
+
+
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -24,6 +31,7 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.PgpDecryptionService;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
+import eu.siacs.conversations.crypto.OtrService;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.UIHelper;
@@ -82,6 +90,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
     protected String hostname = null;
     protected int port = 5222;
     protected boolean online = false;
+    private OtrService mOtrService = null;
     private String rosterVersion;
     private String displayName = null;
     private AxolotlService axolotlService = null;
@@ -396,12 +405,16 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
     }
 
     public void initAccountServices(final XmppConnectionService context) {
+        this.mOtrService = new OtrService(context, this);
         this.axolotlService = new AxolotlService(this, context);
         this.pgpDecryptionService = new PgpDecryptionService(context);
         if (xmppConnection != null) {
             xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
         }
     }
+    public OtrService getOtrService() {
+        return this.mOtrService;
+    }
 
     public PgpDecryptionService getPgpDecryptionService() {
         return this.pgpDecryptionService;
@@ -415,6 +428,26 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         this.xmppConnection = connection;
     }
 
+    public String getOtrFingerprint() {
+        if (this.otrFingerprint == null) {
+            try {
+                if (this.mOtrService == null) {
+                    return null;
+                }
+                final PublicKey publicKey = this.mOtrService.getPublicKey();
+                if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
+                    return null;
+                }
+                this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US);
+                return this.otrFingerprint;
+            } catch (final OtrCryptoException ignored) {
+                return null;
+            }
+        } else {
+            return this.otrFingerprint;
+        }
+    }
+
     public String getRosterVersion() {
         if (this.rosterVersion == null) {
             return "";
@@ -578,6 +611,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
 
     private List<XmppUri.Fingerprint> getFingerprints() {
         ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
+        final String otr = this.getOtrFingerprint();
+        if (otr != null) {
+            fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR, otr));
+        }
         if (axolotlService == null) {
             return fingerprints;
         }
diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java
index 3f8a6bd40..779da8132 100644
--- a/src/main/java/eu/siacs/conversations/entities/Contact.java
+++ b/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -516,6 +516,31 @@ public class Contact implements ListItem, Blockable {
         return avatar;
     }
 
+    public boolean deleteOtrFingerprint(String fingerprint) {
+        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);
+                }
+                return success;
+            } catch (JSONException e) {
+                return false;
+            }
+        }
+    }
+
+
     public boolean mutualPresenceSubscription() {
         return getOption(Options.FROM) && getOption(Options.TO);
     }
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index 1bf810216..9cd6cd435 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -8,6 +8,15 @@ import android.database.Cursor;
 import android.preference.PreferenceManager;
 import android.text.TextUtils;
 
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.Locale;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -82,10 +91,15 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
     private int mode;
     private JSONObject attributes;
     private Jid nextCounterpart;
+    private transient SessionImpl otrSession;
+    private transient String otrFingerprint = null;
+    private Smp mSmp = new Smp();
     private transient MucOptions mucOptions = null;
+    private byte[] symmetricKey;
     private boolean messagesLeftOnServer = true;
     private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
     private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
+    private String mLastReceivedOtrMessageId = null;
     private String mFirstMamReference = null;
 
     public Conversation(final String name, final Account account, final Jid contactJid,
@@ -411,6 +425,18 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         }
     }
 
+    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() == encryptionType)) {
+                    onMessageFound.onMessageFound(message);
+                }
+            }
+        }
+    }
+
+
     public void findUnsentTextMessages(OnMessageFound onMessageFound) {
         final ArrayList<Message> results = new ArrayList<>();
         synchronized (this.messages) {
@@ -582,6 +608,15 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         return getContact().getBlockedJid();
     }
 
+    public String getLastReceivedOtrMessageId() {
+        return this.mLastReceivedOtrMessageId;
+    }
+
+    public void setLastReceivedOtrMessageId(String id) {
+        this.mLastReceivedOtrMessageId = id;
+    }
+
+
     public int countMessages() {
         synchronized (this.messages) {
             return this.messages.size();
@@ -778,6 +813,111 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
     public void setMode(int mode) {
         this.mode = mode;
     }
+    public SessionImpl startOtrSession(String presence, boolean sendStart) {
+        if (this.otrSession != null) {
+            return this.otrSession;
+        } else {
+            final SessionID sessionId = new SessionID(this.getJid().asBareJid().toString(),
+                    presence,
+                    "xmpp");
+            this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
+            try {
+                if (sendStart) {
+                    this.otrSession.startSession();
+                    return this.otrSession;
+                }
+                return this.otrSession;
+            } catch (OtrException e) {
+                return null;
+            }
+        }
+
+    }
+
+    public SessionImpl getOtrSession() {
+        return this.otrSession;
+    }
+
+    public void resetOtrSession() {
+        this.otrFingerprint = null;
+        this.otrSession = null;
+        this.mSmp.hint = null;
+        this.mSmp.secret = null;
+        this.mSmp.status = Smp.STATUS_NONE;
+    }
+
+    public Smp smp() {
+        return mSmp;
+    }
+
+    public boolean startOtrIfNeeded() {
+        if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
+            try {
+                this.otrSession.startSession();
+                return true;
+            } catch (OtrException e) {
+                this.resetOtrSession();
+                return false;
+            }
+        } else {
+            return true;
+        }
+    }
+
+    public boolean endOtrIfNeeded() {
+        if (this.otrSession != null) {
+            if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
+                try {
+                    this.otrSession.endSession();
+                    this.resetOtrSession();
+                    return true;
+                } catch (OtrException e) {
+                    this.resetOtrSession();
+                    return false;
+                }
+            } else {
+                this.resetOtrSession();
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    public boolean hasValidOtrSession() {
+        return this.otrSession != null;
+    }
+
+    public synchronized String getOtrFingerprint() {
+        if (this.otrFingerprint == null) {
+            try {
+                if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
+                    return null;
+                }
+                DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
+                this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
+            } catch (final OtrCryptoException ignored) {
+                return null;
+            } catch (final UnsupportedOperationException ignored) {
+                return null;
+            }
+        }
+        return this.otrFingerprint;
+    }
+
+    public boolean verifyOtrFingerprint() {
+        final String fingerprint = getOtrFingerprint();
+        if (fingerprint != null) {
+            getContact().addOtrFingerprint(fingerprint);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public boolean isOtrFingerprintVerified() {
+        return getContact().getOtrFingerprints().contains(getOtrFingerprint());
+    }
 
     /**
      * short for is Private and Non-anonymous
@@ -814,7 +954,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
     }
 
     public int getNextEncryption() {
-        if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
+        if (!Config.supportOmemo() && !Config.supportOpenPgp() && !Config.supportOtr()) {
             return Message.ENCRYPTION_NONE;
         }
         if (OmemoSetting.isAlways()) {
@@ -830,8 +970,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             defaultEncryption = Message.ENCRYPTION_NONE;
         }
         int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
-        if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
+        if (encryption < 0) {
             return defaultEncryption;
+        } else if (encryption == Message.ENCRYPTION_OTR) {
+            return encryption;
         } else {
             return encryption;
         }
@@ -846,6 +988,11 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         return nextMessage == null ? "" : nextMessage;
     }
 
+    public boolean smpRequested() {
+        return smp().status == Smp.STATUS_CONTACT_REQUESTED;
+    }
+
+
     public @Nullable
     Draft getDraft() {
         final long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
@@ -868,6 +1015,15 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         return changed;
     }
 
+    public void setSymmetricKey(byte[] key) {
+        this.symmetricKey = key;
+    }
+
+    public byte[] getSymmetricKey() {
+        return this.symmetricKey;
+    }
+
+
     public Bookmark getBookmark() {
         return this.account.getBookmark(this.contactJid);
     }
@@ -1240,6 +1396,19 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         }
     }
 
+    public class Smp {
+        public static final int STATUS_NONE = 0;
+        public static final int STATUS_CONTACT_REQUESTED = 1;
+        public static final int STATUS_WE_REQUESTED = 2;
+        public static final int STATUS_FAILED = 3;
+        public static final int STATUS_VERIFIED = 4;
+
+        public String secret = null;
+        public String hint = null;
+        public int status = 0;
+    }
+
+
     public Message findDuplicateMessage(Message message) {
         return findDuplicateMessage(message, false);
     }
diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
index 507c3550d..04e2c3a2d 100644
--- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
@@ -59,6 +59,9 @@ public abstract class AbstractGenerator {
     private final String[] PRIVACY_SENSITIVE = {
             "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone
     };
+    private final String[] OTR = {
+            "urn:xmpp:otr:0"
+    };
     private final String[] VOIP_NAMESPACES = {
             Namespace.JINGLE_TRANSPORT_ICE_UDP,
             Namespace.JINGLE_FEATURE_AUDIO,
@@ -139,6 +142,9 @@ public abstract class AbstractGenerator {
             features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
             features.addAll(Arrays.asList(VOIP_NAMESPACES));
         }
+        if (Config.supportOtr()) {
+            features.addAll(Arrays.asList(OTR));
+        }
         if (mXmppConnectionService.broadcastLastActivity()) {
             features.add(Namespace.IDLE);
         }
diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
index 2389f45e2..17c74c99c 100644
--- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
@@ -6,6 +6,9 @@ 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.Config;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
@@ -24,6 +27,7 @@ import eu.siacs.conversations.xmpp.jingle.Media;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
 public class MessageGenerator extends AbstractGenerator {
+    public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that";
     private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo";
     private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that.";
 
@@ -113,6 +117,31 @@ public class MessageGenerator extends AbstractGenerator {
         packet.addChild("no-permanent-storage", "urn:xmpp:hints"); //do not copy this. this is wrong. it is *store*
     }
 
+
+    public MessagePacket generateOtrChat(Message message) {
+        Conversation conversation = (Conversation) message.getConversation();
+        Session otrSession = conversation.getOtrSession();
+        if (otrSession == null) {
+            return null;
+        }
+        MessagePacket packet = preparePacket(message);
+        addMessageHints(packet);
+        try {
+            String content;
+            if (message.hasFileOnRemoteHost()) {
+                content = message.getFileParams().url.toString();
+            } else {
+                content = message.getBody();
+            }
+            packet.setBody(otrSession.transformSending(content)[0]);
+            packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0");
+            return packet;
+        } catch (OtrException e) {
+            return null;
+        }
+    }
+
+
     public MessagePacket generateChat(Message message) {
         MessagePacket packet = preparePacket(message);
         String content;
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index db2222f25..d83b797f0 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -3,6 +3,12 @@ package eu.siacs.conversations.parser;
 import android.util.Log;
 import android.util.Pair;
 
+import android.os.Build;
+import android.text.Html;
+
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -21,6 +27,7 @@ import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
 import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
 import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
+import eu.siacs.conversations.crypto.OtrService;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Bookmark;
 import eu.siacs.conversations.entities.Contact;
@@ -32,9 +39,11 @@ import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.ReadByMarker;
 import eu.siacs.conversations.entities.ReceiptRequest;
 import eu.siacs.conversations.entities.RtpSessionStatus;
+import eu.siacs.conversations.entities.Presence;
 import eu.siacs.conversations.http.HttpConnectionManager;
 import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.Namespace;
 import eu.siacs.conversations.xml.Element;
@@ -51,7 +60,7 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 
 
 public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
-
+    private static final List<String> CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin", "Adium", "Trillian");
     private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
 
     private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract");
@@ -96,6 +105,32 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         return result != null ? result : fallback;
     }
 
+
+    private static boolean clientMightSendHtml(Account account, Jid from) {
+        String resource = from.getResource();
+        if (resource == null) {
+            return false;
+        }
+        Presence presence = account.getRoster().getContact(from).getPresences().getPresencesMap().get(resource);
+        ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult();
+        if (disco == null) {
+            return false;
+        }
+        return hasIdentityKnowForSendingHtml(disco.getIdentities());
+    }
+
+    private static boolean hasIdentityKnowForSendingHtml(List<ServiceDiscoveryResult.Identity> identities) {
+        for (ServiceDiscoveryResult.Identity identity : identities) {
+            if (identity.getName() != null) {
+                if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+
     private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
         ChatState state = ChatState.parse(packet);
         if (state != null && c != null) {
@@ -127,6 +162,67 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
         return false;
     }
 
+    private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) {
+        String presence;
+        if (from.isBareJid()) {
+            presence = "";
+        } else {
+            presence = from.getResource();
+        }
+        if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) {
+            conversation.endOtrIfNeeded();
+        }
+        if (!conversation.hasValidOtrSession()) {
+            conversation.startOtrSession(presence, false);
+        } else {
+            String foreignPresence = conversation.getOtrSession().getSessionID().getUserID();
+            if (!foreignPresence.equals(presence)) {
+                conversation.endOtrIfNeeded();
+                conversation.startOtrSession(presence, false);
+            }
+        }
+        try {
+            conversation.setLastReceivedOtrMessageId(id);
+            Session otrSession = conversation.getOtrSession();
+            body = otrSession.transformReceiving(body);
+            SessionStatus status = otrSession.getSessionStatus();
+            if (body == null && status == SessionStatus.ENCRYPTED) {
+                mXmppConnectionService.onOtrSessionEstablished(conversation);
+                return null;
+            } else if (body == null && status == SessionStatus.FINISHED) {
+                conversation.resetOtrSession();
+                mXmppConnectionService.updateConversationUi();
+                return null;
+            } else if (body == null || (body.isEmpty())) {
+                return null;
+            }
+            if (body.startsWith(CryptoHelper.FILETRANSFER)) {
+                String key = body.substring(CryptoHelper.FILETRANSFER.length());
+                conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
+                return null;
+            }
+            if (clientMightSendHtml(conversation.getAccount(), from)) {
+                Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OTR message from bad behaving client. escaping HTML…");
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                    body = Html.fromHtml(body, Html.FROM_HTML_MODE_LEGACY).toString();
+                } else {
+                    body = Html.fromHtml(body).toString();
+                }
+            }
+
+            final OtrService otrService = conversation.getAccount().getOtrService();
+            Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED);
+            finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey()));
+            conversation.setLastReceivedOtrMessageId(null);
+
+            return finishedMessage;
+        } catch (Exception e) {
+            conversation.resetOtrSession();
+            return null;
+        }
+    }
+
+
     private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) {
         final AxolotlService service = conversation.getAccount().getAxolotlService();
         final XmppAxolotlMessage xmppAxolotlMessage;
@@ -350,6 +446,14 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                         }
                     }
                 }
+
+                if (message != null) {
+                    if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+                        Conversation conversation = (Conversation) message.getConversation();
+                        conversation.endOtrIfNeeded();
+                    }
+                }
+
             }
             return true;
         }
@@ -509,7 +613,20 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                 }
             }
             final Message message;
-            if (pgpEncrypted != null && Config.supportOpenPgp()) {
+            if (body != null && body.content.startsWith("?OTR") && Config.supportOtr()) {
+                if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) {
+                    message = parseOtrChat(body.content, from, remoteMsgId, conversation);
+                    if (message == null) {
+                        return;
+                    }
+                } else {
+                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed));
+                    message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
+                    if (body.count > 1) {
+                        message.setBodyLanguage(body.language);
+                    }
+                }
+            } else if (pgpEncrypted != null && Config.supportOpenPgp()) {
                 message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
             } else if (axolotlEncrypted != null && Config.supportOmemo()) {
                 Jid origin;
@@ -823,6 +940,14 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                 processMessageReceipts(account, packet, remoteMsgId, query);
             }
 
+
+            if (message.getStatus() == Message.STATUS_RECEIVED
+                    && conversation.getOtrSession() != null
+                    && !conversation.getOtrSession().getSessionID().getUserID()
+                    .equals(message.getCounterpart().getResource())) {
+                conversation.endOtrIfNeeded();
+            }
+
             mXmppConnectionService.databaseBackend.createMessage(message);
             final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
             if ((mXmppConnectionService.easyDownloader() || message.trusted()) && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
diff --git a/src/main/java/eu/siacs/conversations/services/EmojiService.java b/src/main/java/eu/siacs/conversations/services/EmojiService.java
new file mode 100644
index 000000000..daec95252
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/services/EmojiService.java
@@ -0,0 +1,53 @@
+package eu.siacs.conversations.services;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.provider.FontRequest;
+import androidx.emoji.text.EmojiCompat;
+import androidx.emoji.text.FontRequestEmojiCompatConfig;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+
+public class EmojiService {
+
+    private final EmojiCompat.InitCallback initCallback = new EmojiCompat.InitCallback() {
+        @Override
+        public void onInitialized() {
+            super.onInitialized();
+            Log.d(Config.LOGTAG, "EmojiService succeeded in loading fonts");
+
+        }
+
+        @Override
+        public void onFailed(Throwable throwable) {
+            super.onFailed(throwable);
+            Log.d(Config.LOGTAG, "EmojiService failed to load fonts", throwable);
+        }
+    };
+
+    private final Context context;
+
+    public EmojiService(Context context) {
+        this.context = context;
+    }
+
+    public void init(boolean useBundledEmoji) {
+        final FontRequest fontRequest = new FontRequest(
+                "com.google.android.gms.fonts",
+                "com.google.android.gms",
+                "Noto Color Emoji Compat",
+                R.array.font_certs);
+        FontRequestEmojiCompatConfig fontRequestEmojiCompatConfig = new FontRequestEmojiCompatConfig(context, fontRequest);
+        fontRequestEmojiCompatConfig.registerInitCallback(initCallback);
+        //On recent Androids we assume to have the latest emojis
+        //there are some annoying bugs with emoji compat that make it a safer choice not to use it when possible
+        // a) when using the ondemand emoji font (play store) flags don’t work
+        // b) the text preview has annoying glitches when the cut of text contains emojis (the emoji will be half visible)
+        // c) can trigger a hardware rendering bug https://issuetracker.google.com/issues/67102093
+        fontRequestEmojiCompatConfig.setReplaceAll(useBundledEmoji && Build.VERSION.SDK_INT < Build.VERSION_CODES.O);
+        EmojiCompat.init(fontRequestEmojiCompatConfig);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index ab025ab59..09b4a0b38 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -54,6 +54,14 @@ import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.LruCache;
 import android.util.Pair;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
+import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
+import eu.siacs.conversations.xmpp.Jid;
+
 
 import androidx.annotation.BoolRes;
 import androidx.annotation.IntegerRes;
@@ -253,9 +261,18 @@ public class XmppConnectionService extends Service {
         Conversation conversation = find(getConversations(), contact);
         if (conversation != null) {
             if (online) {
+                conversation.endOtrIfNeeded();
                 if (contact.getPresences().size() == 1) {
                     sendUnsentMessages(conversation);
                 }
+            } else {
+                //check if the resource we are haveing a conversation with is still online
+                if (conversation.hasValidOtrSession()) {
+                    String otrResource = conversation.getOtrSession().getSessionID().getUserID();
+                    if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) {
+                        conversation.endOtrIfNeeded();
+                    }
+                }
             }
         }
     };
@@ -436,6 +453,9 @@ public class XmppConnectionService extends Service {
                     if (conversation.getAccount() == account
                             && !pendingJoin
                             && !inProgressJoin) {
+                        if (!conversation.startOtrIfNeeded()) {
+                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": couldn't start OTR with " + conversation.getContact().getJid() + " when needed");
+                        }
                         sendUnsentMessages(conversation);
                     }
                 }
@@ -1770,6 +1790,12 @@ public class XmppConnectionService extends Service {
             }
         }
 
+        if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
+            conversation.endOtrIfNeeded();
+            conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
+                    message1 -> markMessage(message1, Message.STATUS_SEND_FAILED));
+        }
+
         final boolean inProgressJoin = isJoinInProgress(conversation);
 
         if (account.isOnlineAndConnected() && !inProgressJoin) {
@@ -1801,6 +1827,30 @@ public class XmppConnectionService extends Service {
                         packet = mMessageGenerator.generatePgpChat(message);
                     }
                     break;
+                case Message.ENCRYPTION_OTR:
+                    SessionImpl otrSession = conversation.getOtrSession();
+                    if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
+                        try {
+                            message.setCounterpart(OtrJidHelper.fromSessionID(otrSession.getSessionID()));
+                        } catch (IllegalArgumentException e) {
+                            break;
+                        }
+                        if (message.needsUploading()) {
+                            mJingleConnectionManager.startJingleFileTransfer(message);
+                        } else {
+                            packet = mMessageGenerator.generateOtrChat(message);
+                        }
+                    } else if (otrSession == null) {
+                        if (message.fixCounterpart()) {
+                            conversation.startOtrSession(message.getCounterpart().getResource(), true);
+                        } else {
+                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fix counterpart for OTR message to contact " + message.getCounterpart());
+                            break;
+                        }
+                    } else {
+                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + " OTR session with " + message.getContact() + " is in wrong state: " + otrSession.getSessionStatus().toString());
+                    }
+                    break;
                 case Message.ENCRYPTION_AXOLOTL:
                     message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
                     if (message.needsUploading()) {
@@ -1853,6 +1903,12 @@ public class XmppConnectionService extends Service {
                         }
                     }
                     break;
+                case Message.ENCRYPTION_OTR:
+                    if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
+                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": create otr session without starting for " + message.getContact().getJid());
+                        conversation.startOtrSession(message.getCounterpart().getResource(), false);
+                    }
+                    break;
                 case Message.ENCRYPTION_AXOLOTL:
                     message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
                     break;
@@ -3757,6 +3813,12 @@ public class XmppConnectionService extends Service {
                     if (conversation.getAccount() == account) {
                         if (conversation.getMode() == Conversation.MODE_MULTI) {
                             leaveMuc(conversation, true);
+                        } else {
+                            if (conversation.endOtrIfNeeded()) {
+                                Log.d(Config.LOGTAG, account.getJid().asBareJid()
+                                        + ": ended otr session with "
+                                        + conversation.getJid());
+                            }
                         }
                     }
                 }
@@ -3814,6 +3876,66 @@ public class XmppConnectionService extends Service {
         pushContactToServer(contact, preAuth);
     }
 
+    public void onOtrSessionEstablished(Conversation conversation) {
+        final Account account = conversation.getAccount();
+        final Session otrSession = conversation.getOtrSession();
+        Log.d(Config.LOGTAG,
+                account.getJid().asBareJid() + " otr session established with "
+                        + conversation.getJid() + "/"
+                        + otrSession.getSessionID().getUserID());
+        conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
+
+            @Override
+            public void onMessageFound(Message message) {
+                SessionID id = otrSession.getSessionID();
+                try {
+                    message.setCounterpart(Jid.of(id.getAccountID() + "/" + id.getUserID()));
+                } catch (IllegalArgumentException e) {
+                    return;
+                }
+                if (message.needsUploading()) {
+                    mJingleConnectionManager.startJingleFileTransfer(message);
+                } else {
+                    MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
+                    if (outPacket != null) {
+                        mMessageGenerator.addDelay(outPacket, message.getTimeSent());
+                        message.setStatus(Message.STATUS_SEND);
+                        databaseBackend.updateMessage(message, false);
+                        sendMessagePacket(account, outPacket);
+                    }
+                }
+                updateConversationUi();
+            }
+        });
+    }
+
+    public boolean renewSymmetricKey(Conversation conversation) {
+        Account account = conversation.getAccount();
+        byte[] symmetricKey = new byte[32];
+        this.mRandom.nextBytes(symmetricKey);
+        Session otrSession = conversation.getOtrSession();
+        if (otrSession != null) {
+            MessagePacket packet = new MessagePacket();
+            packet.setType(MessagePacket.TYPE_CHAT);
+            packet.setFrom(account.getJid());
+            MessageGenerator.addMessageHints(packet);
+            packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
+                    + otrSession.getSessionID().getUserID());
+            try {
+                packet.setBody(otrSession
+                        .transformSending(CryptoHelper.FILETRANSFER
+                                + CryptoHelper.bytesToHex(symmetricKey))[0]);
+                sendMessagePacket(account, packet);
+                conversation.setSymmetricKey(symmetricKey);
+                return true;
+            } catch (OtrException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+
     public void pushContactToServer(final Contact contact) {
         pushContactToServer(contact, null);
     }
@@ -5163,7 +5285,10 @@ public class XmppConnectionService extends Service {
         boolean performedVerification = false;
         final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
         for (XmppUri.Fingerprint fp : fingerprints) {
-            if (fp.type == XmppUri.FingerprintType.OMEMO) {
+            if (fp.type == XmppUri.FingerprintType.OTR) {
+                performedVerification |= contact.addOtrFingerprint(fp.fingerprint);
+                needsRosterWrite |= performedVerification;
+            } else if (fp.type == XmppUri.FingerprintType.OMEMO) {
                 String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
                 FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
                 if (fingerprintStatus != null) {
diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
index cfaa9d585..721d2f9d9 100644
--- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -51,6 +51,7 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
+import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
@@ -680,6 +681,26 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         binding.detailsContactKeys.removeAllViews();
         boolean hasKeys = false;
         LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        if (Config.supportOtr()) {
+            for (final String otrFingerprint : contact.getOtrFingerprints()) {
+                hasKeys = true;
+                View view = inflater.inflate(R.layout.contact_key, binding.detailsContactKeys, false);
+                TextView key = view.findViewById(R.id.key);
+                TextView keyType = view.findViewById(R.id.key_type);
+                ImageButton removeButton = view
+                        .findViewById(R.id.button_remove);
+                removeButton.setVisibility(View.VISIBLE);
+                key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
+                if (otrFingerprint != null && otrFingerprint.equalsIgnoreCase(messageFingerprint)) {
+                    keyType.setText(R.string.otr_fingerprint_selected_message);
+                    keyType.setTextColor(ContextCompat.getColor(this, R.color.accent));
+                } else {
+                    keyType.setText(R.string.otr_fingerprint);
+                }
+                binding.detailsContactKeys.addView(view);
+                removeButton.setOnClickListener(v -> confirmToDeleteFingerprint(otrFingerprint));
+            }
+        }
         final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
         if (Config.supportOmemo() && axolotlService != null) {
             final Collection<XmppAxolotlSession> sessions = axolotlService.findSessionsForContact(contact);
@@ -768,6 +789,20 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             }
         }
     }
+    protected void confirmToDeleteFingerprint(final String fingerprint) {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setTitle(R.string.delete_fingerprint);
+        builder.setMessage(R.string.sure_delete_fingerprint);
+        builder.setNegativeButton(R.string.cancel, null);
+        builder.setPositiveButton(R.string.delete,
+                (dialog, which) -> {
+                    if (contact.deleteOtrFingerprint(fingerprint)) {
+                        populateView();
+                        xmppConnectionService.syncRosterToDisk(contact.getAccount());
+                    }
+                });
+        builder.create().show();
+    }
 
     public void onBackendConnected() {
         if (accountJid != null && contactJid != null) {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index 1deba4c30..c4b32d857 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -1,5 +1,7 @@
 package eu.siacs.conversations.ui;
 
+import net.java.otr4j.session.SessionStatus;
+
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 import static eu.siacs.conversations.ui.SettingsActivity.HIDE_YOU_ARE_NOT_PARTICIPATING;
@@ -208,6 +210,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
     private Toast messageLoaderToast;
     private ConversationsActivity activity;
     private Menu mOptionsMenu;
+    protected OnClickListener clickToVerify = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            activity.verifyOtrSessionDialog(conversation, v);
+        }
+    };
 
     private final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd  hh:mm (z)", Locale.US);
 
@@ -504,6 +512,18 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
             }
         }
     };
+    private OnClickListener mAnswerSmpClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            Intent intent = new Intent(activity, VerifyOTRActivity.class);
+            intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
+            intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
+            intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
+            intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION);
+            startActivity(intent);
+            activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
+        }
+    };
 
     protected OnClickListener clickToDecryptListener = new OnClickListener() {
 
@@ -917,6 +937,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
             message.setUuid(UUID.randomUUID().toString());
         }
         switch (conversation.getNextEncryption()) {
+            case Message.ENCRYPTION_OTR:
+                sendOtrMessage(message);
+                break;
             case Message.ENCRYPTION_PGP:
                 sendPgpMessage(message);
                 break;
@@ -1604,6 +1627,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
         }
         switch (item.getItemId()) {
             case R.id.encryption_choice_axolotl:
+            case R.id.encryption_choice_otr:
             case R.id.encryption_choice_pgp:
             case R.id.encryption_choice_none:
                 handleEncryptionSelection(item);
@@ -1785,6 +1809,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
                 updated = conversation.setNextEncryption(Message.ENCRYPTION_NONE);
                 item.setChecked(true);
                 break;
+            case R.id.encryption_choice_otr:
+                updated = conversation.setNextEncryption(Message.ENCRYPTION_OTR);
+                item.setChecked(true);
+                break;
             case R.id.encryption_choice_pgp:
                 if (activity.hasPgp()) {
                     if (conversation.getAccount().getPgpSignature() != null) {
@@ -1999,6 +2027,20 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
         activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
     }
 
+    private OnClickListener OTRwarning = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            try {
+                final Uri uri = Uri.parse("https://monocles.wiki/index.php?title=Monocles_chat");
+                Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
+                startActivity(browserIntent);
+            } catch (Exception e) {
+                ToastCompat.makeText(activity, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show();
+            }
+        }
+    };
+
+
     @SuppressLint("InflateParams")
     protected void clearHistoryDialog(final Conversation conversation) {
         final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
@@ -2852,9 +2894,27 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
             }
         } else if (account.hasPendingPgpIntent(conversation)) {
             showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
+        } else if (mode == Conversation.MODE_SINGLE
+                && conversation.smpRequested()) {
+            showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener);
+        } else if (mode == Conversation.MODE_SINGLE
+                && conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
+            showSnackbar(R.string.otr_warning, R.string.readmore, OTRwarning);
+        } else if (mode == Conversation.MODE_SINGLE
+                && conversation.hasValidOtrSession()
+                && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED)
+                && (!conversation.isOtrFingerprintVerified())) {
+            showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify);
+        } else if (connection != null
+                && connection.getFeatures().blocking()
+                && conversation.countMessages() != 0
+                && !conversation.isBlocked()
+                && conversation.isWithStranger()) {
+            showSnackbar(R.string.received_message_from_stranger, R.string.block, mBlockClickListener);
         } else if (activity.warnUnecryptedChat()) {
             if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE && conversation.isSingleOrPrivateAndNonAnonymous() && ((Config.supportOmemo() && Conversation.suitableForOmemoByDefault(conversation)) ||
-                    (Config.supportOpenPgp() && account.isPgpDecryptionServiceConnected()))) {
+                    (Config.supportOpenPgp() && account.isPgpDecryptionServiceConnected()) || (
+                    mode == Conversation.MODE_SINGLE && Config.supportOtr()))) {
                 if (ENCRYPTION_EXCEPTIONS.contains(conversation.getJid().toString()) || conversation.getJid().toString().equals(account.getJid().getDomain())) {
                     hideSnackbar();
                 } else {
@@ -3245,6 +3305,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
         builder.setPositiveButton(getString(R.string.send_unencrypted), listener);
         builder.create().show();
     }
+    protected void sendOtrMessage(final Message message) {
+        final ConversationsActivity activity = (ConversationsActivity) getActivity();
+        final XmppConnectionService xmppService = activity.xmppConnectionService;
+        activity.selectPresence(conversation,
+                () -> {
+                    message.setCounterpart(conversation.getNextCounterpart());
+                    xmppService.sendMessage(message);
+                    messageSent();
+                });
+    }
 
     public void appendText(String text, final boolean doNotAppend) {
         if (text == null) {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
index 400f6f7de..3a805b037 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
@@ -57,11 +57,15 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.TextView;
+import android.widget.Toast;
+
+import net.java.otr4j.session.SessionStatus;
 
 import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.widget.PopupMenu;
 import androidx.appcompat.widget.Toolbar;
 import androidx.databinding.DataBindingUtil;
 
@@ -971,6 +975,32 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
             }
         }
     }
+    public void verifyOtrSessionDialog(final Conversation conversation, View view) {
+        if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
+            ToastCompat.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show();
+            return;
+        }
+        if (view == null) {
+            return;
+        }
+        PopupMenu popup = new PopupMenu(this, view);
+        popup.inflate(R.menu.verification_choices);
+        popup.setOnMenuItemClickListener(menuItem -> {
+            Intent intent = new Intent(ConversationsActivity.this, VerifyOTRActivity.class);
+            intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
+            intent.putExtra("contact", conversation.getContact().getJid().asBareJid().toString());
+            intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
+            switch (menuItem.getItemId()) {
+                case R.id.ask_question:
+                    intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION);
+                    break;
+            }
+            startActivity(intent);
+            overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
+            return true;
+        });
+        popup.show();
+    }
 
     @Override
     public void onConversationArchived(Conversation conversation) {
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
index fe4ed0fb1..d46b6f004 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -36,6 +36,7 @@ import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
 import androidx.databinding.DataBindingUtil;
 
 import com.google.android.material.textfield.TextInputLayout;
@@ -1305,6 +1306,25 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
             } else {
                 this.binding.pgpFingerprintBox.setVisibility(View.GONE);
             }
+            final String otrFingerprint = this.mAccount.getOtrFingerprint();
+            if (otrFingerprint != null && Config.supportOtr()) {
+                if ("otr".equals(messageFingerprint)) {
+                    this.binding.otrFingerprintDesc.setTextColor(ContextCompat.getColor(this, R.color.accent));
+                }
+                this.binding.otrFingerprintBox.setVisibility(View.VISIBLE);
+                this.binding.otrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
+                this.binding.actionCopyToClipboard.setVisibility(View.VISIBLE);
+                this.binding.actionCopyToClipboard.setOnClickListener(v -> {
+                    if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(otrFingerprint), R.string.otr_fingerprint)) {
+                        ToastCompat.makeText(
+                                EditAccountActivity.this,
+                                R.string.toast_message_otr_fingerprint,
+                                Toast.LENGTH_SHORT).show();
+                    }
+                });
+            } else {
+                this.binding.otrFingerprintBox.setVisibility(View.GONE);
+            }
             final String ownAxolotlFingerprint = this.mAccount.getAxolotlService().getOwnFingerprint();
             if (ownAxolotlFingerprint != null && Config.supportOmemo()) {
                 this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE);
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index fd16fe51f..5d4ae1834 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -86,6 +86,7 @@ public class SettingsActivity extends XmppActivity implements
     public static final String MAPPREVIEW_HOST = "mappreview_host";
     public static final String ALLOW_MESSAGE_CORRECTION = "allow_message_correction";
     public static final String ALLOW_MESSAGE_RETRACTION = "allow_message_retraction";
+    public static final String ENABLE_OTR_ENCRYPTION = "enable_otr_encryption";
     public static final String USE_UNICOLORED_CHATBG = "unicolored_chatbg";
     public static final String EASY_DOWNLOADER = "easy_downloader";
     public static final String MIN_ANDROID_SDK21_SHOWN = "min_android_sdk21_shown";
diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java
new file mode 100644
index 000000000..6528fb38a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java
@@ -0,0 +1,450 @@
+package eu.siacs.conversations.ui;
+
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AlertDialog;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.XmppUri;
+import eu.siacs.conversations.xmpp.Jid;
+import me.drakeet.support.toast.ToastCompat;
+
+public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate {
+
+    public static final String ACTION_VERIFY_CONTACT = "verify_contact";
+    public static final int MODE_SCAN_FINGERPRINT = -0x0502;
+    public static final int MODE_ASK_QUESTION = 0x0503;
+    public static final int MODE_ANSWER_QUESTION = 0x0504;
+    public static final int MODE_MANUAL_VERIFICATION = 0x0505;
+
+    private LinearLayout mManualVerificationArea;
+    private LinearLayout mSmpVerificationArea;
+    private TextView mRemoteFingerprint;
+    private TextView mYourFingerprint;
+    private TextView mVerificationExplain;
+    private TextView mStatusMessage;
+    private TextView mSharedSecretHint;
+    private EditText mSharedSecretHintEditable;
+    private EditText mSharedSecretSecret;
+    private Button mLeftButton;
+    private Button mRightButton;
+    private Account mAccount;
+    private Conversation mConversation;
+    private int mode = MODE_MANUAL_VERIFICATION;
+    private XmppUri mPendingUri = null;
+
+    private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() {
+
+        @Override
+        public void onClick(DialogInterface dialogInterface, int click) {
+            mConversation.verifyOtrFingerprint();
+            xmppConnectionService.syncRosterToDisk(mConversation.getAccount());
+            ToastCompat.makeText(VerifyOTRActivity.this, R.string.verified, Toast.LENGTH_SHORT).show();
+            finish();
+        }
+    };
+
+    private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() {
+        @Override
+        public void onClick(final View view) {
+            if (isAccountOnline()) {
+                final String question = mSharedSecretHintEditable.getText().toString();
+                final String secret = mSharedSecretSecret.getText().toString();
+                if (question.trim().isEmpty()) {
+                    mSharedSecretHintEditable.requestFocus();
+                    mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty));
+                } else if (secret.trim().isEmpty()) {
+                    mSharedSecretSecret.requestFocus();
+                    mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty));
+                } else {
+                    mSharedSecretSecret.setError(null);
+                    mSharedSecretHintEditable.setError(null);
+                    initSmp(question, secret);
+                    updateView();
+                }
+            }
+        }
+    };
+    private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            if (isAccountOnline()) {
+                abortSmp();
+                updateView();
+            }
+        }
+    };
+    private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() {
+
+        @Override
+        public void onClick(View view) {
+            if (isAccountOnline()) {
+                final String question = mSharedSecretHintEditable.getText().toString();
+                final String secret = mSharedSecretSecret.getText().toString();
+                respondSmp(question, secret);
+                updateView();
+            }
+        }
+    };
+    private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            mConversation.smp().status = Conversation.Smp.STATUS_NONE;
+            mConversation.smp().hint = null;
+            mConversation.smp().secret = null;
+            updateView();
+        }
+    };
+    private View.OnClickListener mFinishListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            mConversation.smp().status = Conversation.Smp.STATUS_NONE;
+            finish();
+        }
+    };
+
+    protected boolean initSmp(final String question, final String secret) {
+        final Session session = mConversation.getOtrSession();
+        if (session != null) {
+            try {
+                session.initSmp(question, secret);
+                mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED;
+                mConversation.smp().secret = secret;
+                mConversation.smp().hint = question;
+                return true;
+            } catch (OtrException e) {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    protected boolean abortSmp() {
+        final Session session = mConversation.getOtrSession();
+        if (session != null) {
+            try {
+                session.abortSmp();
+                mConversation.smp().status = Conversation.Smp.STATUS_NONE;
+                mConversation.smp().hint = null;
+                mConversation.smp().secret = null;
+                return true;
+            } catch (OtrException e) {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    protected boolean respondSmp(final String question, final String secret) {
+        final Session session = mConversation.getOtrSession();
+        if (session != null) {
+            try {
+                session.respondSmp(question, secret);
+                return true;
+            } catch (OtrException e) {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    protected boolean verifyWithUri(XmppUri uri) {
+        Contact contact = mConversation.getContact();
+        if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) {
+            xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints());
+            ToastCompat.makeText(this, R.string.verified, Toast.LENGTH_SHORT).show();
+            updateView();
+            return true;
+        } else {
+            ToastCompat.makeText(this, R.string.could_not_verify_fingerprint, Toast.LENGTH_SHORT).show();
+            return false;
+        }
+    }
+
+    protected boolean isAccountOnline() {
+        if (this.mAccount.getStatus() != Account.State.ONLINE) {
+            ToastCompat.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    protected boolean handleIntent(Intent intent) {
+        if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) {
+            this.mAccount = extractAccount(intent);
+            if (this.mAccount == null) {
+                return false;
+            }
+            try {
+                this.mConversation = this.xmppConnectionService.find(this.mAccount, Jid.of(intent.getExtras().getString("contact")));
+                if (this.mConversation == null) {
+                    return false;
+                }
+            } catch (final IllegalArgumentException ignored) {
+                ignored.printStackTrace();
+                return false;
+            } catch (Exception e) {
+                e.printStackTrace();
+                return false;
+            }
+            this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION);
+            // todo scan OTR fingerprint
+            if (this.mode == MODE_SCAN_FINGERPRINT) {
+                Log.d(Config.LOGTAG, "Scan OTR fingerprint is not implemented in this version");
+                //new IntentIntegrator(this).initiateScan();
+                return false;
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        // todo onActivityResult for OTR scan
+        Log.d(Config.LOGTAG, "Scan OTR fingerprint result is not implemented in this version");
+        /*if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) {
+            IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
+            if (scanResult != null && scanResult.getFormatName() != null) {
+                String data = scanResult.getContents();
+                XmppUri uri = new XmppUri(data);
+                if (xmppConnectionServiceBound) {
+                    verifyWithUri(uri);
+                    finish();
+                } else {
+                    this.mPendingUri = uri;
+                }
+            } else {
+                finish();
+            }
+        }*/
+        super.onActivityResult(requestCode, requestCode, intent);
+    }
+
+    @Override
+    protected void onBackendConnected() {
+        if (handleIntent(getIntent())) {
+            updateView();
+        } else if (mPendingUri != null) {
+            verifyWithUri(mPendingUri);
+            finish();
+            mPendingUri = null;
+        }
+        setIntent(null);
+    }
+
+    protected void updateView() {
+        if (this.mConversation != null && this.mConversation.hasValidOtrSession()) {
+            final ActionBar actionBar = getSupportActionBar();
+            this.mVerificationExplain.setText(R.string.no_otr_session_found);
+            invalidateOptionsMenu();
+            switch (this.mode) {
+                case MODE_ASK_QUESTION:
+                    if (actionBar != null) {
+                        actionBar.setTitle(R.string.ask_question);
+                    }
+                    this.updateViewAskQuestion();
+                    break;
+                case MODE_ANSWER_QUESTION:
+                    if (actionBar != null) {
+                        actionBar.setTitle(R.string.smp_requested);
+                    }
+                    this.updateViewAnswerQuestion();
+                    break;
+                case MODE_MANUAL_VERIFICATION:
+                default:
+                    if (actionBar != null) {
+                        actionBar.setTitle(R.string.manually_verify);
+                    }
+                    this.updateViewManualVerification();
+                    break;
+            }
+        } else {
+            this.mManualVerificationArea.setVisibility(View.GONE);
+            this.mSmpVerificationArea.setVisibility(View.GONE);
+        }
+    }
+
+    protected void updateViewManualVerification() {
+        this.mVerificationExplain.setText(R.string.manual_verification_explanation);
+        this.mManualVerificationArea.setVisibility(View.VISIBLE);
+        this.mSmpVerificationArea.setVisibility(View.GONE);
+        this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint()));
+        this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint()));
+        if (this.mConversation.isOtrFingerprintVerified()) {
+            deactivateButton(this.mRightButton, R.string.verified);
+            activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener);
+        } else {
+            activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener);
+            activateButton(this.mRightButton, R.string.verify, new View.OnClickListener() {
+                @Override
+                public void onClick(View view) {
+                    showManuallyVerifyDialog();
+                }
+            });
+        }
+    }
+
+    protected void updateViewAskQuestion() {
+        this.mManualVerificationArea.setVisibility(View.GONE);
+        this.mSmpVerificationArea.setVisibility(View.VISIBLE);
+        this.mVerificationExplain.setText(R.string.smp_explain_question);
+        final int smpStatus = this.mConversation.smp().status;
+        switch (smpStatus) {
+            case Conversation.Smp.STATUS_WE_REQUESTED:
+                this.mStatusMessage.setVisibility(View.GONE);
+                this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
+                this.mSharedSecretSecret.setVisibility(View.VISIBLE);
+                this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint);
+                this.mSharedSecretSecret.setText(this.mConversation.smp().secret);
+                this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener);
+                this.deactivateButton(this.mRightButton, R.string.in_progress);
+                break;
+            case Conversation.Smp.STATUS_FAILED:
+                this.mStatusMessage.setVisibility(View.GONE);
+                this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
+                this.mSharedSecretSecret.setVisibility(View.VISIBLE);
+                this.mSharedSecretSecret.requestFocus();
+                this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match));
+                this.deactivateButton(this.mLeftButton, R.string.cancel);
+                this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener);
+                break;
+            case Conversation.Smp.STATUS_VERIFIED:
+                this.mSharedSecretHintEditable.setText("");
+                this.mSharedSecretHintEditable.setVisibility(View.GONE);
+                this.mSharedSecretSecret.setText("");
+                this.mSharedSecretSecret.setVisibility(View.GONE);
+                this.mStatusMessage.setVisibility(View.VISIBLE);
+                this.deactivateButton(this.mLeftButton, R.string.cancel);
+                this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
+                break;
+            default:
+                this.mStatusMessage.setVisibility(View.GONE);
+                this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
+                this.mSharedSecretSecret.setVisibility(View.VISIBLE);
+                this.activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener);
+                this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener);
+                break;
+        }
+    }
+
+    protected void updateViewAnswerQuestion() {
+        this.mManualVerificationArea.setVisibility(View.GONE);
+        this.mSmpVerificationArea.setVisibility(View.VISIBLE);
+        this.mVerificationExplain.setText(R.string.smp_explain_answer);
+        this.mSharedSecretHintEditable.setVisibility(View.GONE);
+        this.mSharedSecretHint.setVisibility(View.VISIBLE);
+        this.deactivateButton(this.mLeftButton, R.string.cancel);
+        final int smpStatus = this.mConversation.smp().status;
+        switch (smpStatus) {
+            case Conversation.Smp.STATUS_CONTACT_REQUESTED:
+                this.mStatusMessage.setVisibility(View.GONE);
+                this.mSharedSecretHint.setText(this.mConversation.smp().hint);
+                this.activateButton(this.mRightButton, R.string.respond, this.mRespondSharedSecretListener);
+                break;
+            case Conversation.Smp.STATUS_VERIFIED:
+                this.mSharedSecretHintEditable.setText("");
+                this.mSharedSecretHintEditable.setVisibility(View.GONE);
+                this.mSharedSecretHint.setVisibility(View.GONE);
+                this.mSharedSecretSecret.setText("");
+                this.mSharedSecretSecret.setVisibility(View.GONE);
+                this.mStatusMessage.setVisibility(View.VISIBLE);
+                this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
+                break;
+            case Conversation.Smp.STATUS_FAILED:
+            default:
+                this.mSharedSecretSecret.requestFocus();
+                this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match));
+                this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
+                break;
+        }
+    }
+
+    protected void activateButton(Button button, int text, View.OnClickListener listener) {
+        button.setEnabled(true);
+        button.setText(text);
+        button.setOnClickListener(listener);
+    }
+
+    protected void deactivateButton(Button button, int text) {
+        button.setEnabled(false);
+        button.setText(text);
+        button.setOnClickListener(null);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_verify_otr);
+        this.mRemoteFingerprint = findViewById(R.id.remote_fingerprint);
+        this.mYourFingerprint = findViewById(R.id.your_fingerprint);
+        this.mLeftButton = findViewById(R.id.left_button);
+        this.mRightButton = findViewById(R.id.right_button);
+        this.mVerificationExplain = findViewById(R.id.verification_explanation);
+        this.mStatusMessage = findViewById(R.id.status_message);
+        this.mSharedSecretSecret = findViewById(R.id.shared_secret_secret);
+        this.mSharedSecretHintEditable = findViewById(R.id.shared_secret_hint_editable);
+        this.mSharedSecretHint = findViewById(R.id.shared_secret_hint);
+        this.mManualVerificationArea = findViewById(R.id.manual_verification_area);
+        this.mSmpVerificationArea = findViewById(R.id.smp_verification_area);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(final Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        getMenuInflater().inflate(R.menu.verify_otr, menu);
+        return true;
+    }
+
+    private void showManuallyVerifyDialog() {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setTitle(R.string.manually_verify);
+        builder.setMessage(R.string.are_you_sure_verify_fingerprint);
+        builder.setNegativeButton(R.string.cancel, null);
+        builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener);
+        builder.create().show();
+    }
+
+    @Override
+    protected String getShareableUri() {
+        if (mAccount != null) {
+            return mAccount.getShareableUri();
+        } else {
+            return "";
+        }
+    }
+
+    public void onConversationUpdate() {
+        refreshUi();
+    }
+
+    @Override
+    protected void refreshUiReal() {
+        updateView();
+    }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
index 9c569abb7..c35304c78 100644
--- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -3,6 +3,12 @@ package eu.siacs.conversations.ui;
 import static eu.siacs.conversations.ui.SettingsActivity.USE_BUNDLED_EMOJIS;
 import static eu.siacs.conversations.ui.SettingsActivity.USE_INTERNAL_UPDATER;
 
+import android.util.Pair;
+import net.java.otr4j.session.SessionID;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import eu.siacs.conversations.utils.CryptoHelper;
 import android.Manifest;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
@@ -106,6 +112,8 @@ import eu.siacs.conversations.xmpp.XmppConnection;
 import me.drakeet.support.toast.ToastCompat;
 import pl.droidsonroids.gif.GifDrawable;
 
+import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_OTR_ENCRYPTION;
+
 public abstract class XmppActivity extends ActionBarActivity {
 
     protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
@@ -419,6 +427,19 @@ public abstract class XmppActivity extends ActionBarActivity {
 
     public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
         final Contact contact = conversation.getContact();
+        if (conversation.hasValidOtrSession()) {
+            SessionID id = conversation.getOtrSession().getSessionID();
+            Jid jid;
+            try {
+                jid = Jid.of(id.getAccountID() + "/" + id.getUserID());
+            } catch (IllegalArgumentException e) {
+                jid = null;
+            }
+            conversation.setNextCounterpart(jid);
+            listener.onPresenceSelected();
+        } else if (!contact.showInRoster()) {
+            showAddToRosterDialog(conversation);
+        }
         if (contact.showInRoster() || contact.isSelf()) {
             final Presences presences = contact.getPresences();
             if (presences.size() == 0) {
@@ -489,7 +510,9 @@ public abstract class XmppActivity extends ActionBarActivity {
     public boolean unicoloredBG() {
         return getBooleanPreference("unicolored_chatbg", R.bool.use_unicolored_chatbg) || getPreferences().getString(SettingsActivity.THEME, getString(R.string.theme)).equals("black");
     }
-
+    public boolean enableOTR() {
+        return getBooleanPreference(ENABLE_OTR_ENCRYPTION, R.bool.enable_otr);
+    }
     public boolean showDateInQuotes() {
         return getBooleanPreference("show_date_in_quotes", R.bool.show_date_in_quotes);
     }
@@ -909,7 +932,58 @@ public abstract class XmppActivity extends ActionBarActivity {
         }
     }
 
-    protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
+        private void showPresenceSelectionDialog(Presences presences, final Conversation conversation, final OnPresenceSelected listener) {
+            final Contact contact = conversation.getContact();
+            final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+            builder.setTitle(getString(R.string.choose_presence));
+            final String[] resourceArray = presences.toResourceArray();
+            Pair<Map<String, String>, Map<String, String>> typeAndName = presences.toTypeAndNameMap();
+            final Map<String, String> resourceTypeMap = typeAndName.first;
+            final Map<String, String> resourceNameMap = typeAndName.second;
+            final String[] readableIdentities = new String[resourceArray.length];
+            final AtomicInteger selectedResource = new AtomicInteger(0);
+            for (int i = 0; i < resourceArray.length; ++i) {
+                String resource = resourceArray[i];
+                if (resource.equals(contact.getLastResource())) {
+                    selectedResource.set(i);
+                }
+                String type = resourceTypeMap.get(resource);
+                String name = resourceNameMap.get(resource);
+                if (type != null) {
+                    if (Collections.frequency(resourceTypeMap.values(), type) == 1) {
+                        readableIdentities[i] = PresenceSelector.translateType(this, type);
+                    } else if (name != null) {
+                        if (Collections.frequency(resourceNameMap.values(), name) == 1
+                                || CryptoHelper.UUID_PATTERN.matcher(resource).matches()) {
+                            readableIdentities[i] = PresenceSelector.translateType(this, type) + "  (" + name + ")";
+                        } else {
+                            readableIdentities[i] = PresenceSelector.translateType(this, type) + " (" + name + " / " + resource + ")";
+                        }
+                    } else {
+                        readableIdentities[i] = PresenceSelector.translateType(this, type) + " (" + resource + ")";
+                    }
+                } else {
+                    readableIdentities[i] = resource;
+                }
+            }
+            builder.setSingleChoiceItems(readableIdentities,
+                    selectedResource.get(),
+                    (dialog, which) -> selectedResource.set(which));
+            builder.setNegativeButton(R.string.cancel, null);
+            builder.setPositiveButton(R.string.ok, (dialog, which) -> {
+                try {
+                    Jid next = Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), resourceArray[selectedResource.get()]);
+                    conversation.setNextCounterpart(next);
+                } catch (IllegalArgumentException e) {
+                    conversation.setNextCounterpart(null);
+                }
+                listener.onPresenceSelected();
+            });
+            builder.create().show();
+        }
+
+
+        protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
         if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
             mPendingConferenceInvite = ConferenceInvite.parse(data);
diff --git a/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java b/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java
index 59a94a508..fb3706773 100644
--- a/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java
+++ b/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java
@@ -132,6 +132,7 @@ public class ConversationMenuConfigurator {
         if (conversation.getNextEncryption() != Message.ENCRYPTION_NONE) {
             menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
         }
+        otr.setVisible(Config.supportOtr() && activity.enableOTR());
         if (conversation.getMode() == Conversation.MODE_MULTI) {
             otr.setVisible(false);
         }
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
index 58edbef5b..6288c81e1 100644
--- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -594,6 +594,8 @@ public class UIHelper {
                 } else {
                     return context.getString(R.string.send_message_to_x, conversation.getName());
                 }
+            case Message.ENCRYPTION_OTR:
+                return context.getString(R.string.send_otr_message);
             case Message.ENCRYPTION_AXOLOTL:
                 AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
                 if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java b/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java
index 8d3d8ead9..09c4dec53 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jid/OtrJidHelper.java
@@ -1,9 +1,17 @@
+
 package eu.siacs.conversations.xmpp.jid;
 
+import net.java.otr4j.session.SessionID;
 
 import eu.siacs.conversations.xmpp.Jid;
 
 public final class OtrJidHelper {
 
-
+    public static Jid fromSessionID(final SessionID id) throws IllegalArgumentException {
+        if (id.getUserID().isEmpty()) {
+            return Jid.of(id.getAccountID());
+        } else {
+            return Jid.of(id.getAccountID() + "/" + id.getUserID());
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml
index be16aa780..98ef4e5f3 100644
--- a/src/main/res/layout/activity_edit_account.xml
+++ b/src/main/res/layout/activity_edit_account.xml
@@ -758,6 +758,57 @@
                             </LinearLayout>
                         </RelativeLayout>
 
+                        <RelativeLayout
+                            android:id="@+id/otr_fingerprint_box"
+                            android:layout_width="wrap_content"
+                            android:layout_height="match_parent"
+                            android:layout_marginTop="24dp">
+
+                            <LinearLayout
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_alignParentStart="true"
+                                android:layout_alignParentLeft="true"
+                                android:layout_centerVertical="true"
+                                android:layout_toStartOf="@+id/key_actions"
+                                android:layout_toLeftOf="@+id/key_actions"
+                                android:orientation="vertical">
+
+                                <TextView
+                                    android:id="@+id/otr_fingerprint"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:textAppearance="@style/TextAppearance.Conversations.Fingerprint" />
+
+                                <TextView
+                                    android:id="@+id/otr_fingerprint_desc"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:text="@string/otr_fingerprint"
+                                    android:textAppearance="@style/TextAppearance.Conversations.Caption" />
+
+                            </LinearLayout>
+
+                            <LinearLayout
+                                android:id="@+id/key_actions"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:layout_alignParentEnd="true"
+                                android:layout_alignParentRight="true"
+                                android:layout_centerVertical="true"
+                                android:orientation="horizontal">
+
+                                <ImageButton
+                                    android:id="@+id/action_copy_to_clipboard"
+                                    android:layout_width="wrap_content"
+                                    android:layout_height="wrap_content"
+                                    android:background="?attr/selectableItemBackgroundBorderless"
+                                    android:contentDescription="@string/copy_otr_clipboard_description"
+                                    android:padding="@dimen/image_button_padding"
+                                    android:src="?attr/icon_copy"
+                                    android:visibility="visible" />
+                            </LinearLayout>
+                        </RelativeLayout>
                         <RelativeLayout
                             android:id="@+id/axolotl_fingerprint_box"
                             android:layout_width="wrap_content"
diff --git a/src/main/res/menu/verification_choices.xml b/src/main/res/menu/verification_choices.xml
index 57d45dcde..cad8dee9a 100644
--- a/src/main/res/menu/verification_choices.xml
+++ b/src/main/res/menu/verification_choices.xml
@@ -1,9 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <item
-        android:id="@+id/scan_fingerprint"
-        android:title="@string/scan_qr_code" />
     <item
         android:id="@+id/ask_question"
         android:title="@string/ask_question" />
diff --git a/src/main/res/values/about.xml b/src/main/res/values/about.xml
index 0d82c88ec..804430a6e 100644
--- a/src/main/res/values/about.xml
+++ b/src/main/res/values/about.xml
@@ -46,6 +46,7 @@
             \n\nhttp://leafletjs.com/\n(BSD 2-Clause)
             \n\nhttps://www.openstreetmap.org/\n(Open Database License)
             \n\nhttp://xmpp.rocks/\n(The MIT License (MIT))
+		    \n\nhttps://github.com/jitsi/otr4j\n(LGPL-3.0)
             \n\nhttps://github.com/drakeet/ToastCompat/\n(Apache License, Version 2.0)
             \n\nhttps://github.com/leinardi/FloatingActionButtonSpeedDial/\n(Apache License, Version 2.0)
             \n\nhttps://github.com/PonnamKarthik/RichLinkPreview\n(Apache License, Version 2.0)
diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml
index 7ff48ca5d..b72e9bcbc 100644
--- a/src/main/res/values/defaults.xml
+++ b/src/main/res/values/defaults.xml
@@ -63,6 +63,7 @@
     <bool name="use_internal_updater">true</bool>
     <bool name="show_own_accounts">true</bool>
     <bool name="vibrate_in_chat">true</bool>
+    <bool name="enable_otr">false</bool>
     <bool name="showtextformatting">true</bool>
     <integer name="auto_accept_filesize_wifi">10485760</integer>
     <integer name="auto_accept_filesize_mobile">524288</integer>
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 8e7d4141a..f76efe109 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -836,6 +836,19 @@
     <string name="pref_play_gif_inside">Play GIF files in chat</string>
     <string name="pref_play_gif_inside_summary">Play GIF files directly inside the chat view.</string>
     <string name="open_with">Open with…</string>
+    <string name="send_otr_message">Send OTR encrypted message</string>
+    <string name="unknown_otr_fingerprint">Unknown OTR fingerprint</string>
+    <string name="otr_fingerprint">OTR fingerprint</string>
+    <string name="otr_fingerprint_selected_message">OTR fingerprint of message</string>
+    <string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string>
+    <string name="verify_otr">Verify OTR</string>
+    <string name="no_otr_session_found">No valid OTR session has been found!</string>
+    <string name="are_you_sure_verify_fingerprint">Are you sure that you want to verify your contacts OTR fingerprint?</string>
+    <string name="copy_otr_clipboard_description">Copy OTR fingerprint to clipboard</string>
+    <string name="otr_session_not_started">Send a message to start an encrypted chat</string>
+    <string name="pref_enable_otr_summary">Enable OTR encryption for message encryption</string>
+    <string name="pref_enable_otr">Enable OTR encryption</string>
+    <string name="otr_warning">The support of OTR encryption is in the beta mode. Click read more to get more information. A link in a browser will open.</string>
     <string name="server_info_adhoc_invite">XEP-0050: Ad-Hoc Commands: user invite</string>
     <string name="choose_account">Choose account</string>
     <string name="set_profile_picture">monocles chat account picture</string>
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
index c7b642457..4386c6875 100644
--- a/src/main/res/xml/preferences.xml
+++ b/src/main/res/xml/preferences.xml
@@ -489,6 +489,11 @@
                 android:key="delete_omemo_identities"
                 android:summary="@string/pref_delete_omemo_identities_summary"
                 android:title="@string/pref_delete_omemo_identities" />
+            <CheckBoxPreference
+                android:defaultValue="@bool/enable_otr"
+                android:key="enable_otr_encryption"
+                android:summary="@string/pref_enable_otr_summary"
+                android:title="@string/pref_enable_otr" />
             <CheckBoxPreference
                 android:defaultValue="@bool/dont_trust_system_cas"
                 android:key="dont_trust_system_cas"