diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
index 667b003e7e..1da2302b74 100644
--- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint
 import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
 import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
 import org.thoughtcrime.securesms.loki.protocol.MultiDeviceOpenGroupUpdateJob;
-import org.thoughtcrime.securesms.loki.PushBackgroundMessageSendJob;
+import org.thoughtcrime.securesms.loki.protocol.PushEphemeralMessageSendJob;
 import org.thoughtcrime.securesms.loki.PushMessageSyncSendJob;
 
 import java.util.Arrays;
@@ -72,7 +72,7 @@ public final class JobManagerFactories {
       put(TypingSendJob.KEY,                         new TypingSendJob.Factory());
       put(UpdateApkJob.KEY,                          new UpdateApkJob.Factory());
       put(PushMessageSyncSendJob.KEY,                new PushMessageSyncSendJob.Factory());
-      put(PushBackgroundMessageSendJob.KEY,          new PushBackgroundMessageSendJob.Factory());
+      put(PushEphemeralMessageSendJob.KEY,          new PushEphemeralMessageSendJob.Factory());
       put(MultiDeviceOpenGroupUpdateJob.KEY,         new MultiDeviceOpenGroupUpdateJob.Factory());
     }};
   }
diff --git a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt
deleted file mode 100644
index b83a8a517e..0000000000
--- a/src/org/thoughtcrime/securesms/loki/PushBackgroundMessageSendJob.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package org.thoughtcrime.securesms.loki
-
-import org.thoughtcrime.securesms.ApplicationContext
-import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
-import org.thoughtcrime.securesms.database.Address
-import org.thoughtcrime.securesms.database.DatabaseFactory
-import org.thoughtcrime.securesms.jobmanager.Data
-import org.thoughtcrime.securesms.jobmanager.Job
-import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
-import org.thoughtcrime.securesms.jobs.BaseJob
-import org.thoughtcrime.securesms.logging.Log
-import org.thoughtcrime.securesms.recipients.Recipient
-import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
-import org.whispersystems.signalservice.api.push.SignalServiceAddress
-import org.whispersystems.signalservice.internal.util.JsonUtil
-import java.io.IOException
-import java.util.concurrent.TimeUnit
-
-data class BackgroundMessage private constructor(val data: Map<String, Any>) {
-
-  companion object {
-
-    @JvmStatic
-    fun create(recipient: String) = BackgroundMessage(mapOf("recipient" to recipient))
-
-    @JvmStatic
-    fun createFriendRequest(recipient: String, messageBody: String) = BackgroundMessage(mapOf( "recipient" to recipient, "body" to messageBody, "friendRequest" to true ))
-
-    @JvmStatic
-    fun createUnpairingRequest(recipient: String) = BackgroundMessage(mapOf( "recipient" to recipient, "unpairingRequest" to true ))
-
-    @JvmStatic
-    fun createSessionRestore(recipient: String) = BackgroundMessage(mapOf( "recipient" to recipient, "friendRequest" to true, "sessionRestore" to true ))
-
-    @JvmStatic
-    fun createSessionRequest(recipient: String) = BackgroundMessage(mapOf("recipient" to recipient, "friendRequest" to true, "sessionRequest" to true))
-            
-    internal fun parse(serialized: String): BackgroundMessage {
-      val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map<String, Any> ?: throw AssertionError("JSON parsing failed")
-      return BackgroundMessage(data)
-    }
-  }
-
-  fun <T> get(key: String, defaultValue: T): T {
-    return data[key] as? T ?: defaultValue
-  }
-
-  fun serialize(): String {
-    return JsonUtil.toJson(data)
-  }
-}
-
-class PushBackgroundMessageSendJob private constructor(
-    parameters: Parameters,
-    private val message: BackgroundMessage
-) : BaseJob(parameters) {
-  companion object {
-    const val KEY = "PushBackgroundMessageSendJob"
-
-    private val TAG = PushBackgroundMessageSendJob::class.java.simpleName
-
-    private val KEY_MESSAGE = "message"
-  }
-
-  constructor(message: BackgroundMessage) : this(Parameters.Builder()
-          .addConstraint(NetworkConstraint.KEY)
-          .setQueue(KEY)
-          .setLifespan(TimeUnit.DAYS.toMillis(1))
-          .setMaxAttempts(1)
-          .build(),
-          message)
-
-  override fun serialize(): Data {
-    return Data.Builder()
-            .putString(KEY_MESSAGE, message.serialize())
-            .build()
-  }
-
-  override fun getFactoryKey(): String {
-    return KEY
-  }
-
-  public override fun onRun() {
-    val recipient = message.get<String?>("recipient", null) ?: throw IllegalStateException()
-    val dataMessage = SignalServiceDataMessage.newBuilder()
-            .withTimestamp(System.currentTimeMillis())
-            .withBody(message.get<String?>("body", null))
-
-    if (message.get("friendRequest", false)) {
-      val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient)
-      dataMessage.withPreKeyBundle(bundle)
-              .asFriendRequest(true)
-    }
-
-    if (message.get("unpairingRequest", false)) {
-      dataMessage.asUnlinkingRequest(true)
-    }
-
-    if (message.get("sessionRestore", false)) {
-      dataMessage.asSessionRestorationRequest(true)
-    }
-
-    if (message.get("sessionRequest", false)) {
-      dataMessage.asSessionRequest(true)
-    }
-
-    val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
-    val address = SignalServiceAddress(recipient)
-    try {
-      val udAccess = UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(recipient), false))
-      messageSender.sendMessage(-1, address, udAccess, dataMessage.build()) // The message ID doesn't matter
-    } catch (e: Exception) {
-      Log.d("Loki", "Failed to send background message to: ${recipient}.")
-      throw e
-    }
-  }
-
-  public override fun onShouldRetry(e: Exception): Boolean {
-    // Loki - Disable since we have our own retrying when sending messages
-    return false
-  }
-
-  override fun onCanceled() {}
-
-  class Factory : Job.Factory<PushBackgroundMessageSendJob> {
-    override fun create(parameters: Parameters, data: Data): PushBackgroundMessageSendJob {
-      try {
-        val messageJSON = data.getString(KEY_MESSAGE)
-        return PushBackgroundMessageSendJob(parameters, BackgroundMessage.parse(messageJSON))
-      } catch (e: IOException) {
-        throw AssertionError(e)
-      }
-    }
-  }
-}
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt
index 89230d1d29..0b7b2c3aac 100644
--- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt
+++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt
@@ -1,6 +1,7 @@
 package org.thoughtcrime.securesms.loki.protocol
 
 import android.content.Context
+import org.thoughtcrime.securesms.ApplicationContext
 import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore
 import org.thoughtcrime.securesms.database.Address
 import org.thoughtcrime.securesms.database.DatabaseFactory
@@ -79,9 +80,13 @@ object ClosedGroupsProtocol {
         for (device in allDevices) {
             val address = SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID)
             val hasSession = TextSecureSessionStore(context).containsSession(address)
-            if (!hasSession) {
-                MessageSender.sendBackgroundSessionRequest(context, device)
-            }
+            if (!hasSession) { sendSessionRequest(context, device) }
         }
     }
+
+    @JvmStatic
+    fun sendSessionRequest(context: Context, publicKey: String) {
+        val sessionRequest = EphemeralMessage.createSessionRequest(publicKey)
+        ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRequest))
+    }
 }
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/EphemeralMessage.kt b/src/org/thoughtcrime/securesms/loki/protocol/EphemeralMessage.kt
new file mode 100644
index 0000000000..e9c8377101
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/protocol/EphemeralMessage.kt
@@ -0,0 +1,34 @@
+package org.thoughtcrime.securesms.loki.protocol
+
+import org.whispersystems.signalservice.internal.util.JsonUtil
+
+data class EphemeralMessage private constructor(val data: Map<*, *>) {
+
+    companion object {
+
+        @JvmStatic
+        fun create(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey ))
+
+        @JvmStatic
+        fun createUnlinkingRequest(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey, "unpairingRequest" to true ))
+
+        @JvmStatic
+        fun createSessionRestorationRequest(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey, "friendRequest" to true, "sessionRestore" to true ))
+
+        @JvmStatic
+        fun createSessionRequest(publicKey: String) = EphemeralMessage(mapOf("recipient" to publicKey, "friendRequest" to true, "sessionRequest" to true))
+
+        internal fun parse(serialized: String): EphemeralMessage {
+            val data = JsonUtil.fromJson(serialized, Map::class.java) ?: throw IllegalArgumentException("Couldn't parse string to JSON")
+            return EphemeralMessage(data)
+        }
+    }
+
+    fun <T> get(key: String, defaultValue: T): T {
+        return data[key] as? T ?: defaultValue
+    }
+
+    fun serialize(): String {
+        return JsonUtil.toJson(data)
+    }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/FriendRequestProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/FriendRequestProtocol.kt
new file mode 100644
index 0000000000..266c47ddb3
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/protocol/FriendRequestProtocol.kt
@@ -0,0 +1,46 @@
+package org.thoughtcrime.securesms.loki.protocol
+
+import android.content.Context
+import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
+import org.thoughtcrime.securesms.sms.OutgoingTextMessage
+import org.thoughtcrime.securesms.util.TextSecurePreferences
+import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
+import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
+
+object FriendRequestProtocol {
+
+    @JvmStatic
+    fun shouldUpdateFriendRequestStatusFromOutgoingTextMessage(context: Context, message: OutgoingTextMessage): Boolean {
+        // The order of these checks matters
+        if (message.recipient.isGroupRecipient) { return false }
+        if (message.recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return false }
+        // TODO: Return false if the message is a device linking request
+        // TODO: Return false if the message is a session request
+        return message.isFriendRequest
+    }
+
+    @JvmStatic
+    fun shouldUpdateFriendRequestStatusFromOutgoingMediaMessage(context: Context, message: OutgoingMediaMessage): Boolean {
+        // The order of these checks matters
+        if (message.recipient.isGroupRecipient) { return false }
+        if (message.recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return false }
+        // TODO: Return false if the message is a device linking request
+        // TODO: Return false if the message is a session request
+        return message.isFriendRequest
+    }
+
+    @JvmStatic
+    fun setFriendRequestStatusToSendingIfNeeded(context: Context, messageID: Long, threadID: Long) {
+        val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
+        val messageFRStatus = messageDB.getFriendRequestStatus(messageID)
+        if (messageFRStatus == LokiMessageFriendRequestStatus.NONE || messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_EXPIRED) {
+            messageDB.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_SENDING)
+        }
+        val threadDB = DatabaseFactory.getLokiThreadDatabase(context)
+        val threadFRStatus = threadDB.getFriendRequestStatus(threadID)
+        if (threadFRStatus == LokiThreadFriendRequestStatus.NONE || threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) {
+            threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_SENDING)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/LokiSessionResetImplementation.kt b/src/org/thoughtcrime/securesms/loki/protocol/LokiSessionResetImplementation.kt
index 4920fc17c2..0cec3cb624 100644
--- a/src/org/thoughtcrime/securesms/loki/protocol/LokiSessionResetImplementation.kt
+++ b/src/org/thoughtcrime/securesms/loki/protocol/LokiSessionResetImplementation.kt
@@ -2,33 +2,30 @@ package org.thoughtcrime.securesms.loki.protocol
 
 import android.content.Context
 import org.thoughtcrime.securesms.database.DatabaseFactory
-import org.thoughtcrime.securesms.sms.MessageSender
 import org.whispersystems.libsignal.loki.LokiSessionResetProtocol
 import org.whispersystems.libsignal.loki.LokiSessionResetStatus
 import org.whispersystems.libsignal.protocol.PreKeySignalMessage
 
 class LokiSessionResetImplementation(private val context: Context) : LokiSessionResetProtocol {
 
-  override fun getSessionResetStatus(hexEncodedPublicKey: String): LokiSessionResetStatus {
-    return DatabaseFactory.getLokiThreadDatabase(context).getSessionResetStatus(hexEncodedPublicKey)
-  }
-
-  override fun setSessionResetStatus(hexEncodedPublicKey: String, sessionResetStatus: LokiSessionResetStatus) {
-    return DatabaseFactory.getLokiThreadDatabase(context).setSessionResetStatus(hexEncodedPublicKey, sessionResetStatus)
-  }
+    override fun getSessionResetStatus(hexEncodedPublicKey: String): LokiSessionResetStatus {
+        return DatabaseFactory.getLokiThreadDatabase(context).getSessionResetStatus(hexEncodedPublicKey)
+    }
 
-  override fun onNewSessionAdopted(hexEncodedPublicKey: String, oldSessionResetStatus: LokiSessionResetStatus) {
-    if (oldSessionResetStatus == LokiSessionResetStatus.IN_PROGRESS) {
-      // Send a message back to the contact to finalise session reset
-      MessageSender.sendBackgroundMessage(context, hexEncodedPublicKey)
+    override fun setSessionResetStatus(hexEncodedPublicKey: String, sessionResetStatus: LokiSessionResetStatus) {
+        return DatabaseFactory.getLokiThreadDatabase(context).setSessionResetStatus(hexEncodedPublicKey, sessionResetStatus)
     }
 
-    // TODO: Show session reset succeed message
-  }
+    override fun onNewSessionAdopted(hexEncodedPublicKey: String, oldSessionResetStatus: LokiSessionResetStatus) {
+        if (oldSessionResetStatus == LokiSessionResetStatus.IN_PROGRESS) {
+            SessionMetaProtocol.sendEphemeralMessage(context, hexEncodedPublicKey)
+        }
+        // TODO: Show session reset succeed message
+    }
 
-  override fun validatePreKeySignalMessage(sender: String, message: PreKeySignalMessage) {
-    val preKeyRecord = DatabaseFactory.getLokiPreKeyRecordDatabase(context).getPreKeyRecord(sender)
-    check(preKeyRecord != null) { "Received a background message from a user without an associated pre key record." }
-    check(preKeyRecord.id == (message.preKeyId ?: -1)) { "Received a background message from an unknown source." }
-  }
+    override fun validatePreKeySignalMessage(sender: String, message: PreKeySignalMessage) {
+        val preKeyRecord = DatabaseFactory.getLokiPreKeyRecordDatabase(context).getPreKeyRecord(sender)
+        check(preKeyRecord != null) { "Received a background message from a user without an associated pre key record." }
+        check(preKeyRecord.id == (message.preKeyId ?: -1)) { "Received a background message from an unknown source." }
+    }
 }
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt
index ee53934038..2c6ed90ff0 100644
--- a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt
+++ b/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt
@@ -18,61 +18,59 @@ import javax.inject.Inject
 
 class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters) : BaseJob(parameters), InjectableType {
 
-  companion object {
-    const val KEY = "MultiDeviceOpenGroupUpdateJob"
-  }
+    companion object {
+        const val KEY = "MultiDeviceOpenGroupUpdateJob"
+    }
 
-  @Inject
-  lateinit var messageSender: SignalServiceMessageSender
+    @Inject
+    lateinit var messageSender: SignalServiceMessageSender
 
-  constructor() : this(Parameters.Builder()
-          .addConstraint(NetworkConstraint.KEY)
-          .setQueue("MultiDeviceOpenGroupUpdateJob")
-          .setLifespan(TimeUnit.DAYS.toMillis(1))
-          .setMaxAttempts(Parameters.UNLIMITED)
-          .build())
+    constructor() : this(Parameters.Builder()
+        .addConstraint(NetworkConstraint.KEY)
+        .setQueue("MultiDeviceOpenGroupUpdateJob")
+        .setLifespan(TimeUnit.DAYS.toMillis(1))
+        .setMaxAttempts(Parameters.UNLIMITED)
+        .build())
 
-  override fun getFactoryKey(): String { return KEY
-  }
+    override fun getFactoryKey(): String { return KEY }
 
-  override fun serialize(): Data { return Data.EMPTY }
+    override fun serialize(): Data { return Data.EMPTY }
 
-  @Throws(Exception::class)
-  public override fun onRun() {
-    if (!TextSecurePreferences.isMultiDevice(context)) {
-      Log.d("Loki", "Not multi device; aborting...")
-      return
+    @Throws(Exception::class)
+    public override fun onRun() {
+        if (!TextSecurePreferences.isMultiDevice(context)) {
+            Log.d("Loki", "Not multi device; aborting...")
+            return
+        }
+        // Gather open groups
+        val openGroups = mutableListOf<LokiPublicChat>()
+        DatabaseFactory.getGroupDatabase(context).groups.use { reader ->
+            while (true) {
+                val record = reader.next ?: return@use
+                if (!record.isOpenGroup) { continue; }
+                val threadID = GroupManager.getThreadIDFromGroupID(record.encodedId, context)
+                val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
+                if (openGroup != null) { openGroups.add(openGroup) }
+            }
+        }
+        // Send the message
+        if (openGroups.size > 0) {
+            messageSender.sendMessage(SignalServiceSyncMessage.forOpenGroups(openGroups), UnidentifiedAccessUtil.getAccessForSync(context))
+        } else {
+            Log.d("Loki", "No open groups to sync.")
+        }
     }
 
-    val openGroups = mutableListOf<LokiPublicChat>()
-    DatabaseFactory.getGroupDatabase(context).groups.use { reader ->
-      while (true) {
-        val record = reader.next ?: return@use
-        if (!record.isOpenGroup) { continue; }
-
-        val threadID = GroupManager.getThreadIDFromGroupID(record.encodedId, context)
-        val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
-        if (openGroup != null) { openGroups.add(openGroup) }
-      }
+    public override fun onShouldRetry(exception: Exception): Boolean {
+        return false
     }
 
-    if (openGroups.size > 0) {
-      messageSender.sendMessage(SignalServiceSyncMessage.forOpenGroups(openGroups), UnidentifiedAccessUtil.getAccessForSync(context))
-    } else {
-      Log.d("Loki", "No open groups to sync.")
-    }
-  }
-
-  public override fun onShouldRetry(exception: Exception): Boolean {
-    return false
-  }
-
-  override fun onCanceled() { }
+    override fun onCanceled() { }
 
-  class Factory : Job.Factory<MultiDeviceOpenGroupUpdateJob> {
+    class Factory : Job.Factory<MultiDeviceOpenGroupUpdateJob> {
 
-    override fun create(parameters: Parameters, data: Data): MultiDeviceOpenGroupUpdateJob {
-      return MultiDeviceOpenGroupUpdateJob(parameters)
+        override fun create(parameters: Parameters, data: Data): MultiDeviceOpenGroupUpdateJob {
+            return MultiDeviceOpenGroupUpdateJob(parameters)
+        }
     }
-  }
 }
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt
new file mode 100644
index 0000000000..aa0c7d823f
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt
@@ -0,0 +1,86 @@
+package org.thoughtcrime.securesms.loki.protocol
+
+import android.content.Context
+import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.recipients.Recipient
+
+object MultiDeviceProtocol {
+
+    @JvmStatic
+    fun sendUnlinkingRequest(context: Context, publicKey: String) {
+        val unlinkingRequest = EphemeralMessage.createUnlinkingRequest(publicKey)
+        ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(unlinkingRequest))
+    }
+
+    @JvmStatic
+    fun sendTextPush(context: Context, recipient: Recipient, messageID: Long) {
+
+    }
+
+    @JvmStatic
+    fun sendMediaPush(context: Context, recipient: Recipient, messageID: Long) {
+
+    }
+
+//    private static void sendMessagePush(Context context, MessageType type, Recipient recipient, long messageId) {
+//        JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
+//
+//        // Just send the message normally if it's a group message or we're sending to one of our devices
+//        String recipientHexEncodedPublicKey = recipient.getAddress().serialize();
+//        if (GeneralUtilitiesKt.isPublicChat(context, recipientHexEncodedPublicKey) || PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false)) {
+//            if (type == MessageType.MEDIA) {
+//                PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress(), false);
+//            } else {
+//                jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
+//            }
+//            return;
+//        }
+//
+//        // If we get here then we are sending a message to a device that is not ours
+//        boolean[] hasSentSyncMessage = { false };
+//        MultiDeviceUtilities.getAllDevicePublicKeysWithFriendStatus(context, recipientHexEncodedPublicKey).success(devices -> {
+//            int friendCount = MultiDeviceUtilities.getFriendCount(context, devices.keySet());
+//            Util.runOnMain(() -> {
+//            ArrayList<Job> jobs = new ArrayList<>();
+//            for (Map.Entry<String, Boolean> entry : devices.entrySet()) {
+//            String deviceHexEncodedPublicKey = entry.getKey();
+//            boolean isFriend = entry.getValue();
+//
+//            Address address = Address.fromSerialized(deviceHexEncodedPublicKey);
+//            long messageIDToUse = recipientHexEncodedPublicKey.equals(deviceHexEncodedPublicKey) ? messageId : -1L;
+//
+//            if (isFriend) {
+//                // Send a normal message if the user is friends with the recipient
+//                // We should also send a sync message if we haven't already sent one
+//                boolean shouldSendSyncMessage = !hasSentSyncMessage[0] && address.isPhone();
+//                if (type == MessageType.MEDIA) {
+//                    jobs.add(new PushMediaSendJob(messageId, messageIDToUse, address, false, null, shouldSendSyncMessage));
+//                } else {
+//                    jobs.add(new PushTextSendJob(messageId, messageIDToUse, address, shouldSendSyncMessage));
+//                }
+//                if (shouldSendSyncMessage) { hasSentSyncMessage[0] = true; }
+//            } else {
+//                // Send friend requests to non-friends. If the user is friends with any
+//                // of the devices then send out a default friend request message.
+//                boolean isFriendsWithAny = (friendCount > 0);
+//                String defaultFriendRequestMessage = isFriendsWithAny ? "Please accept to enable messages to be synced across devices" : null;
+//                if (type == MessageType.MEDIA) {
+//                    jobs.add(new PushMediaSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage, false));
+//                } else {
+//                    jobs.add(new PushTextSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage, false));
+//                }
+//            }
+//        }
+//
+//            // Start the send
+//            if (type == MessageType.MEDIA) {
+//                PushMediaSendJob.enqueue(context, jobManager, (List<PushMediaSendJob>)(List)jobs);
+//            } else {
+//                // Schedule text send jobs
+//                jobManager.startChain(jobs).enqueue();
+//            }
+//        });
+//            return Unit.INSTANCE;
+//        });
+//    }
+}
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/PushEphemeralMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/PushEphemeralMessageSendJob.kt
new file mode 100644
index 0000000000..048aa97e8a
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/protocol/PushEphemeralMessageSendJob.kt
@@ -0,0 +1,98 @@
+package org.thoughtcrime.securesms.loki.protocol
+
+import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
+import org.thoughtcrime.securesms.database.Address
+import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.jobmanager.Data
+import org.thoughtcrime.securesms.jobmanager.Job
+import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
+import org.thoughtcrime.securesms.jobs.BaseJob
+import org.thoughtcrime.securesms.logging.Log
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
+import org.whispersystems.signalservice.api.push.SignalServiceAddress
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+class PushEphemeralMessageSendJob private constructor(
+    parameters: Parameters,
+    private val message: EphemeralMessage
+) : BaseJob(parameters) {
+
+    companion object {
+
+        const val KEY = "PushBackgroundMessageSendJob"
+
+        private val KEY_MESSAGE = "message"
+    }
+
+    constructor(message: EphemeralMessage) : this(Parameters.Builder()
+        .addConstraint(NetworkConstraint.KEY)
+        .setQueue(KEY)
+        .setLifespan(TimeUnit.DAYS.toMillis(1))
+        .setMaxAttempts(1)
+        .build(),
+        message)
+
+    override fun serialize(): Data {
+        return Data.Builder()
+            .putString(KEY_MESSAGE, message.serialize())
+            .build()
+    }
+
+    override fun getFactoryKey(): String {
+        return KEY
+    }
+
+    public override fun onRun() {
+        val recipient = message.get<String?>("recipient", null) ?: throw IllegalStateException()
+        val dataMessage = SignalServiceDataMessage.newBuilder()
+            .withTimestamp(System.currentTimeMillis())
+            .withBody(message.get<String?>("body", null))
+        // Attach a pre key bundle if needed
+        if (message.get("friendRequest", false)) {
+            val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient)
+            dataMessage.withPreKeyBundle(bundle).asFriendRequest(true)
+        }
+        // Set flags if needed
+        if (message.get("unpairingRequest", false)) {
+            dataMessage.asUnlinkingRequest(true)
+        }
+        if (message.get("sessionRestore", false)) {
+            dataMessage.asSessionRestorationRequest(true)
+        }
+        if (message.get("sessionRequest", false)) {
+            dataMessage.asSessionRequest(true)
+        }
+        // Send the message
+        val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
+        val address = SignalServiceAddress(recipient)
+        try {
+            val udAccess = UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(recipient), false))
+            messageSender.sendMessage(-1, address, udAccess, dataMessage.build()) // The message ID doesn't matter
+        } catch (e: Exception) {
+            Log.d("Loki", "Failed to send background message to: $recipient due to error: $e.")
+            throw e
+        }
+    }
+
+    public override fun onShouldRetry(e: Exception): Boolean {
+        // Disable since we have our own retrying
+        return false
+    }
+
+    override fun onCanceled() { }
+
+    class Factory : Job.Factory<PushEphemeralMessageSendJob> {
+
+      override fun create(parameters: Parameters, data: Data): PushEphemeralMessageSendJob {
+          try {
+              val messageJSON = data.getString(KEY_MESSAGE)
+              return PushEphemeralMessageSendJob(parameters, EphemeralMessage.parse(messageJSON))
+          } catch (e: IOException) {
+              throw AssertionError(e)
+          }
+      }
+    }
+}
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt
index 80b5cf6d83..c0f7e7a3e9 100644
--- a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt
+++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt
@@ -22,4 +22,10 @@ object SessionManagementProtocol {
             ApplicationContext.getInstance(context).jobManager.add(CleanPreKeysJob())
         }
     }
+
+    @JvmStatic
+    fun sendSessionRestorationRequest(context: Context, publicKey: String) {
+        val sessionRestorationRequest = EphemeralMessage.createSessionRestorationRequest(publicKey)
+        ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRestorationRequest))
+    }
 }
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt
index f667321bdc..1f93126208 100644
--- a/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt
+++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt
@@ -1,6 +1,7 @@
 package org.thoughtcrime.securesms.loki.protocol
 
 import android.content.Context
+import org.thoughtcrime.securesms.ApplicationContext
 import org.thoughtcrime.securesms.database.Address
 import org.thoughtcrime.securesms.database.DatabaseFactory
 import org.thoughtcrime.securesms.recipients.Recipient
@@ -8,6 +9,12 @@ import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendReque
 
 object SessionMetaProtocol {
 
+    @JvmStatic
+    fun sendEphemeralMessage(context: Context, publicKey: String) {
+        val ephemeralMessage = EphemeralMessage.create(publicKey)
+        ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
+    }
+
     /**
      * Should be invoked for the recipient's master device.
      */
diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt
index 715479a24f..5f4a1a88d6 100644
--- a/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt
+++ b/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt
@@ -1,10 +1,13 @@
 package org.thoughtcrime.securesms.loki.protocol
 
 import android.content.Context
+import org.thoughtcrime.securesms.ApplicationContext
 import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData
 import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData
 import org.thoughtcrime.securesms.database.Address
 import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob
+import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob
 import org.thoughtcrime.securesms.recipients.Recipient
 import org.thoughtcrime.securesms.util.TextSecurePreferences
 import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
@@ -14,8 +17,8 @@ import java.util.*
 object SyncMessagesProtocol {
 
     @JvmStatic
-    fun shouldSyncReadReceipt(address: Address): Boolean {
-        return !address.isGroup
+    fun syncAllContacts(context: Context) {
+        ApplicationContext.getInstance(context).jobManager.add(MultiDeviceContactUpdateJob(context, true))
     }
 
     @JvmStatic
@@ -41,4 +44,19 @@ object SyncMessagesProtocol {
         val isFriend = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS
         return isFriend
     }
+
+    @JvmStatic
+    fun syncAllClosedGroups(context: Context) {
+        ApplicationContext.getInstance(context).jobManager.add(MultiDeviceGroupUpdateJob())
+    }
+
+    @JvmStatic
+    fun syncAllOpenGroups(context: Context) {
+        ApplicationContext.getInstance(context).jobManager.add(MultiDeviceOpenGroupUpdateJob())
+    }
+
+    @JvmStatic
+    fun shouldSyncReadReceipt(address: Address): Boolean {
+        return !address.isGroup
+    }
 }
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java
index 390656ea56..36a3912a2a 100644
--- a/src/org/thoughtcrime/securesms/sms/MessageSender.java
+++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java
@@ -33,127 +33,29 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
 import org.thoughtcrime.securesms.database.ThreadDatabase;
 import org.thoughtcrime.securesms.database.model.MessageRecord;
 import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
-import org.thoughtcrime.securesms.jobmanager.Job;
 import org.thoughtcrime.securesms.jobmanager.JobManager;
 import org.thoughtcrime.securesms.jobs.MmsSendJob;
-import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
-import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
 import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
-import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
-import org.thoughtcrime.securesms.jobs.PushTextSendJob;
 import org.thoughtcrime.securesms.jobs.SmsSendJob;
-import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
-import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
 import org.thoughtcrime.securesms.logging.Log;
-import org.thoughtcrime.securesms.loki.BackgroundMessage;
-import org.thoughtcrime.securesms.loki.FriendRequestHandler;
-import org.thoughtcrime.securesms.loki.protocol.MultiDeviceOpenGroupUpdateJob;
-import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
-import org.thoughtcrime.securesms.loki.PushBackgroundMessageSendJob;
-import org.thoughtcrime.securesms.loki.PushMessageSyncSendJob;
-import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
+import org.thoughtcrime.securesms.loki.protocol.FriendRequestProtocol;
+import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
 import org.thoughtcrime.securesms.mms.MmsException;
 import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
 import org.thoughtcrime.securesms.push.AccountManagerFactory;
 import org.thoughtcrime.securesms.recipients.Recipient;
 import org.thoughtcrime.securesms.service.ExpiringMessageManager;
 import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.Util;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.SignalServiceAccountManager;
 import org.whispersystems.signalservice.api.push.ContactTokenDetails;
-import org.whispersystems.signalservice.loki.protocol.multidevice.LokiDeviceLinkUtilities;
-import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus;
-import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import kotlin.Unit;
 
 public class MessageSender {
 
   private static final String TAG = MessageSender.class.getSimpleName();
 
-  private enum MessageType { TEXT, MEDIA }
-
-  public static void syncAllContacts(Context context, Address recipient) {
-    ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context, recipient, true));
-  }
-
-  public static void syncAllGroups(Context context) {
-    ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceGroupUpdateJob());
-  }
-
-  public static void syncAllOpenGroups(Context context) {
-    ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceOpenGroupUpdateJob());
-  }
-
-  /**
-   * Send a contact sync message to all our devices telling them that we want to sync `contact`
-   */
-  public static void syncContact(Context context, Address contact) {
-    // Don't bother sending a contact sync message if it's one of our devices that we want to sync across
-    MultiDeviceUtilities.isOneOfOurDevices(context, contact).success(isOneOfOurDevice -> {
-      if (!isOneOfOurDevice) {
-        ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context, contact));
-      }
-      return Unit.INSTANCE;
-    });
-  }
-
-  public static void sendBackgroundMessageToAllDevices(Context context, String contactHexEncodedPublicKey) {
-    // Send the background message to the original pubkey
-    sendBackgroundMessage(context, contactHexEncodedPublicKey);
-
-    // Go through the other devices and only send background messages if we're friends or we have received friend request
-    LokiDeviceLinkUtilities.INSTANCE.getAllLinkedDeviceHexEncodedPublicKeys(contactHexEncodedPublicKey).success(devices -> {
-      Util.runOnMain(() -> {
-        for (String device : devices) {
-          // Don't send message to the device we already have sent to
-          if (device.equals(contactHexEncodedPublicKey)) { continue; }
-          Recipient recipient = Recipient.from(context, Address.fromSerialized(device), false);
-          long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient);
-          if (threadID < 0) { continue; }
-          LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID);
-          if (friendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
-            sendBackgroundMessage(context, device);
-          } else if (friendRequestStatus == LokiThreadFriendRequestStatus.NONE || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) {
-            sendBackgroundFriendRequest(context, device, "Please accept to enable messages to be synced across devices");
-          }
-        }
-      });
-      return Unit.INSTANCE;
-    });
-  }
-
-  // region Background message
-
-  // We don't call the message sender here directly and instead we just opt to create a specific job for the send
-  // This is because calling message sender directly would cause the application to freeze in some cases as it was blocking the thread when waiting for a response from the send
-  public static void sendBackgroundMessage(Context context, String contactHexEncodedPublicKey) {
-    ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.create(contactHexEncodedPublicKey)));
-  }
-
-  public static void sendBackgroundFriendRequest(Context context, String contactHexEncodedPublicKey, String messageBody) {
-    ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createFriendRequest(contactHexEncodedPublicKey, messageBody)));
-  }
-
-  public static void sendUnpairRequest(Context context, String contactHexEncodedPublicKey) {
-    ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createUnpairingRequest(contactHexEncodedPublicKey)));
-  }
-
-  public static void sendRestoreSessionMessage(Context context, String contactHexEncodedPublicKey) {
-    ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRestore(contactHexEncodedPublicKey)));
-  }
-
-  public static void sendBackgroundSessionRequest(Context context, String contactHexEncodedPublicKey) {
-    ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRequest(contactHexEncodedPublicKey)));
-  }
-  // endregion
-
   public static long send(final Context context,
                           final OutgoingTextMessage message,
                           final long threadId,
@@ -174,9 +76,9 @@ public class MessageSender {
 
     long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener);
 
-    // Loki - Set the message's friend request status as soon as it has hit the database
-    if (message.isFriendRequest) {
-      FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageId, allocatedThreadId);
+    // Loki - Set the message's friend request status as soon as it hits the database
+    if (FriendRequestProtocol.shouldUpdateFriendRequestStatusFromOutgoingTextMessage(context, message)) {
+      FriendRequestProtocol.setFriendRequestStatusToSendingIfNeeded(context, messageId, allocatedThreadId);
     }
 
     sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
@@ -190,73 +92,32 @@ public class MessageSender {
                           final boolean forceSms,
                           final SmsDatabase.InsertListener insertListener)
   {
-    ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
-    MmsDatabase    database       = DatabaseFactory.getMmsDatabase(context);
+    try {
+      ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+      MmsDatabase    database       = DatabaseFactory.getMmsDatabase(context);
 
-    long allocatedThreadId;
+      long allocatedThreadId;
 
-    if (threadId == -1) {
-      allocatedThreadId = threadDatabase.getThreadIdFor(message.getRecipient(), message.getDistributionType());
-    } else {
-      allocatedThreadId = threadId;
-    }
-
-    Recipient recipient = message.getRecipient();
-
-    // Loki - Turn into a GIF message if possible
-    if (message.getLinkPreviews().isEmpty() && message.getAttachments().isEmpty() && LinkPreviewUtil.isWhitelistedMediaUrl(message.getBody())) {
-      new LinkPreviewRepository(context).fetchGIF(context, message.getBody(), attachmentOrNull -> Util.runOnMain(() -> {
-        Attachment attachment = attachmentOrNull.orNull();
-        try {
-          if (attachment != null) { message.getAttachments().add(attachment); }
-          long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
-          // Loki - Set the message's friend request status as soon as it has hit the database
-          if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) {
-            FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
-          }
-          sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
-        } catch (Exception e) {
-          Log.w(TAG, e);
-          // TODO: Handle
-        }
-      }));
-    } else {
-      try {
-        long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
-        // Loki - Set the message's friend request status as soon as it has hit the database
-        if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) {
-          FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
-        }
-        sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
-      } catch (MmsException e) {
-        Log.w(TAG, e);
-        return threadId;
+      if (threadId == -1) {
+        allocatedThreadId = threadDatabase.getThreadIdFor(message.getRecipient(), message.getDistributionType());
+      } else {
+        allocatedThreadId = threadId;
       }
-    }
 
-    return allocatedThreadId;
-  }
+      Recipient recipient = message.getRecipient();
+      long      messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
 
-  public static void sendSyncMessageToOurDevices(final Context context,
-                                                 final long    messageID,
-                                                 final long    timestamp,
-                                                 final byte[]  message,
-                                                 final int     ttl) {
-    String ourPublicKey = TextSecurePreferences.getLocalNumber(context);
-    JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
-    LokiDeviceLinkUtilities.INSTANCE.getAllLinkedDeviceHexEncodedPublicKeys(ourPublicKey).success(devices -> {
-      Util.runOnMain(() -> {
-        for (String device : devices) {
-          // Don't send to ourselves
-          if (device.equals(ourPublicKey)) { continue; }
-
-          // Create a send job for our device
-          Address address = Address.fromSerialized(device);
-          jobManager.add(new PushMessageSyncSendJob(messageID, address, timestamp, message, ttl));
-        }
-      });
-      return Unit.INSTANCE;
-    });
+      // Loki - Set the message's friend request status as soon as it hits the database
+      if (FriendRequestProtocol.shouldUpdateFriendRequestStatusFromOutgoingMediaMessage(context, message)) {
+        FriendRequestProtocol.setFriendRequestStatusToSendingIfNeeded(context, messageId, allocatedThreadId);
+      }
+
+      sendMediaMessage(context, recipient, forceSms, messageId, message.getExpiresIn());
+      return allocatedThreadId;
+    } catch (MmsException e) {
+      Log.w(TAG, e);
+      return threadId;
+    }
   }
 
   public static void resendGroupMessage(Context context, MessageRecord messageRecord, Address filterAddress) {
@@ -301,73 +162,11 @@ public class MessageSender {
   }
 
   private static void sendTextPush(Context context, Recipient recipient, long messageId) {
-    sendMessagePush(context, MessageType.TEXT, recipient, messageId);
+    MultiDeviceProtocol.sendTextPush(context, recipient, messageId);
   }
 
   private static void sendMediaPush(Context context, Recipient recipient, long messageId) {
-    sendMessagePush(context, MessageType.MEDIA, recipient, messageId);
-  }
-
-  private static void sendMessagePush(Context context, MessageType type, Recipient recipient, long messageId) {
-    JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
-
-    // Just send the message normally if it's a group message or we're sending to one of our devices
-    String recipientHexEncodedPublicKey = recipient.getAddress().serialize();
-    if (GeneralUtilitiesKt.isPublicChat(context, recipientHexEncodedPublicKey) || PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false)) {
-      if (type == MessageType.MEDIA) {
-        PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress(), false);
-      } else {
-        jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
-      }
-      return;
-    }
-
-    // If we get here then we are sending a message to a device that is not ours
-    boolean[] hasSentSyncMessage = { false };
-    MultiDeviceUtilities.getAllDevicePublicKeysWithFriendStatus(context, recipientHexEncodedPublicKey).success(devices -> {
-      int friendCount = MultiDeviceUtilities.getFriendCount(context, devices.keySet());
-      Util.runOnMain(() -> {
-        ArrayList<Job> jobs = new ArrayList<>();
-        for (Map.Entry<String, Boolean> entry : devices.entrySet()) {
-          String deviceHexEncodedPublicKey = entry.getKey();
-          boolean isFriend = entry.getValue();
-
-          Address address = Address.fromSerialized(deviceHexEncodedPublicKey);
-          long messageIDToUse = recipientHexEncodedPublicKey.equals(deviceHexEncodedPublicKey) ? messageId : -1L;
-
-          if (isFriend) {
-            // Send a normal message if the user is friends with the recipient
-            // We should also send a sync message if we haven't already sent one
-            boolean shouldSendSyncMessage = !hasSentSyncMessage[0] && address.isPhone();
-            if (type == MessageType.MEDIA) {
-              jobs.add(new PushMediaSendJob(messageId, messageIDToUse, address, false, null, shouldSendSyncMessage));
-            } else {
-              jobs.add(new PushTextSendJob(messageId, messageIDToUse, address, shouldSendSyncMessage));
-            }
-            if (shouldSendSyncMessage) { hasSentSyncMessage[0] = true; }
-          } else {
-            // Send friend requests to non-friends. If the user is friends with any
-            // of the devices then send out a default friend request message.
-            boolean isFriendsWithAny = (friendCount > 0);
-            String defaultFriendRequestMessage = isFriendsWithAny ? "Please accept to enable messages to be synced across devices" : null;
-            if (type == MessageType.MEDIA) {
-              jobs.add(new PushMediaSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage, false));
-            } else {
-              jobs.add(new PushTextSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage, false));
-            }
-          }
-        }
-
-        // Start the send
-        if (type == MessageType.MEDIA) {
-          PushMediaSendJob.enqueue(context, jobManager, (List<PushMediaSendJob>)(List)jobs);
-        } else {
-          // Schedule text send jobs
-          jobManager.startChain(jobs).enqueue();
-        }
-      });
-      return Unit.INSTANCE;
-    });
+    MultiDeviceProtocol.sendMediaPush(context, recipient, messageId);
   }
 
   private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) {