Refactor MessageSender

pull/192/head
nielsandriesse 4 years ago
parent 819f414446
commit eb5b8886d4

@ -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());
}};
}

@ -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)
}
}
}
}

@ -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))
}
}

@ -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)
}
}

@ -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)
}
}
}

@ -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." }
}
}

@ -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)
}
}
}
}

@ -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;
// });
// }
}

@ -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)
}
}
}
}

@ -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))
}
}

@ -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.
*/

@ -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
}
}

@ -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) {

Loading…
Cancel
Save