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) { - - 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 ?: throw AssertionError("JSON parsing failed") - return BackgroundMessage(data) - } - } - - fun 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("recipient", null) ?: throw IllegalStateException() - val dataMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(System.currentTimeMillis()) - .withBody(message.get("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 { - 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 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() + 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() - 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 { + class Factory : Job.Factory { - 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 jobs = new ArrayList<>(); +// for (Map.Entry 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)(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("recipient", null) ?: throw IllegalStateException() + val dataMessage = SignalServiceDataMessage.newBuilder() + .withTimestamp(System.currentTimeMillis()) + .withBody(message.get("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 { + + 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 jobs = new ArrayList<>(); - for (Map.Entry 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)(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) {