diff --git a/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt b/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt index 3ad5a23399..1a7690fdb6 100644 --- a/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt +++ b/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt @@ -33,7 +33,7 @@ class DatabaseAttachmentDTO { val isUploaded: Boolean = false fun toProto(): SignalServiceProtos.AttachmentPointer? { - val builder = org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer.newBuilder() + val builder = SignalServiceProtos.AttachmentPointer.newBuilder() builder.contentType = this.contentType if (!this.fileName.isNullOrEmpty()) { @@ -46,12 +46,12 @@ class DatabaseAttachmentDTO { builder.size = this.size builder.key = this.key builder.digest = this.digest - builder.flags = if (this.isVoiceNote) org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0 + builder.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0 //TODO I did copy the behavior of iOS below, not sure if that's relevant here... if (this.shouldHaveImageSize) { - if (this.width < kotlin.Int.MAX_VALUE && this.height < kotlin.Int.MAX_VALUE) { - val imageSize: Size = Size(this.width, this.height) + if (this.width < Int.MAX_VALUE && this.height < Int.MAX_VALUE) { + val imageSize= Size(this.width, this.height) val imageWidth = round(imageSize.width.toDouble()) val imageHeight = round(imageSize.height.toDouble()) if (imageWidth > 0 && imageHeight > 0) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/Configuration.kt b/libsession/src/main/java/org/session/libsession/messaging/MessagingConfiguration.kt similarity index 74% rename from libsession/src/main/java/org/session/libsession/messaging/Configuration.kt rename to libsession/src/main/java/org/session/libsession/messaging/MessagingConfiguration.kt index c544a972c7..50f524cba8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/Configuration.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/MessagingConfiguration.kt @@ -1,12 +1,14 @@ package org.session.libsession.messaging +import android.content.Context import org.session.libsession.database.MessageDataProvider import org.session.libsignal.libsignal.loki.SessionResetProtocol import org.session.libsignal.libsignal.state.* import org.session.libsignal.metadata.certificate.CertificateValidator import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol -class Configuration( +class MessagingConfiguration( + val context: Context, val storage: StorageProtocol, val signalStorage: SignalProtocolStore, val sskDatabase: SharedSenderKeysDatabaseProtocol, @@ -15,9 +17,10 @@ class Configuration( val certificateValidator: CertificateValidator) { companion object { - lateinit var shared: Configuration + lateinit var shared: MessagingConfiguration - fun configure(storage: StorageProtocol, + fun configure(context: Context, + storage: StorageProtocol, signalStorage: SignalProtocolStore, sskDatabase: SharedSenderKeysDatabaseProtocol, messageDataProvider: MessageDataProvider, @@ -25,7 +28,7 @@ class Configuration( certificateValidator: CertificateValidator ) { if (Companion::shared.isInitialized) { return } - shared = Configuration(storage, signalStorage, sskDatabase, messageDataProvider, sessionResetImp, certificateValidator) + shared = MessagingConfiguration(context, storage, signalStorage, sskDatabase, messageDataProvider, sessionResetImp, certificateValidator) } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index f9aa62107b..e0d880d94c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -4,9 +4,12 @@ import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.opengroups.OpenGroup +import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.GroupRecord import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECPrivateKey +import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer interface StorageProtocol { @@ -21,7 +24,7 @@ interface StorageProtocol { // Signal Protocol - fun getOrGenerateRegistrationID(): Int //TODO needs impl + fun getOrGenerateRegistrationID(): Int // Shared Sender Keys fun getClosedGroupPrivateKey(publicKey: String): ECPrivateKey? @@ -71,8 +74,20 @@ interface StorageProtocol { fun getReceivedMessageTimestamps(): Set fun addReceivedMessageTimestamp(timestamp: Long) + // Closed Groups + fun getGroup(groupID: String): GroupRecord? + fun createGroup(groupId: String, title: String?, members: List
, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List
) + fun setActive(groupID: String, value: Boolean) + fun removeMember(groupID: String, member: Address) + fun updateMembers(groupID: String, members: List
) + // Settings + fun setProfileSharing(address: Address, value: Boolean) + + // Thread + fun getOrCreateThreadIdFor(address: Address): String + fun getThreadIdFor(address: Address): String? fun getSessionRequestSentTimestamp(publicKey: String): Long? fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 0054a77b1d..6fbf1d8f12 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -4,7 +4,7 @@ import kotlin.math.min import kotlin.math.pow import java.util.Timer -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsignal.libsignal.logging.Log import kotlin.concurrent.schedule @@ -25,7 +25,7 @@ class JobQueue : JobDelegate { fun addWithoutExecuting(job: Job) { job.id = System.currentTimeMillis().toString() - Configuration.shared.storage.persist(job) + MessagingConfiguration.shared.storage.persist(job) job.delegate = this } @@ -37,7 +37,7 @@ class JobQueue : JobDelegate { hasResumedPendingJobs = true val allJobTypes = listOf(AttachmentDownloadJob.collection, AttachmentDownloadJob.collection, MessageReceiveJob.collection, MessageSendJob.collection, NotifyPNServerJob.collection) allJobTypes.forEach { type -> - val allPendingJobs = Configuration.shared.storage.getAllPendingJobs(type) + val allPendingJobs = MessagingConfiguration.shared.storage.getAllPendingJobs(type) allPendingJobs.sortedBy { it.id }.forEach { job -> Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.") job.delegate = this @@ -47,12 +47,12 @@ class JobQueue : JobDelegate { } override fun handleJobSucceeded(job: Job) { - Configuration.shared.storage.markJobAsSucceeded(job) + MessagingConfiguration.shared.storage.markJobAsSucceeded(job) } override fun handleJobFailed(job: Job, error: Exception) { job.failureCount += 1 - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage if (storage.isJobCanceled(job)) { return Log.i("Jobs", "${job::class.simpleName} canceled.")} storage.persist(job) if (job.failureCount == job.maxFailureCount) { @@ -69,7 +69,7 @@ class JobQueue : JobDelegate { override fun handleJobFailedPermanently(job: Job, error: Exception) { job.failureCount += 1 - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage storage.persist(job) storage.markJobAsFailed(job) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt index fb5e3d34b5..5f9ee29c6b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt @@ -1,6 +1,6 @@ package org.session.libsession.messaging.jobs -class MessageReceiveJob : Job { +class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job { override var delegate: JobDelegate? = null override var id: String? = null override var failureCount: Int = 0 diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt index d213c08ad4..9036ce47b5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt @@ -1,5 +1,9 @@ package org.session.libsession.messaging.messages +import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.threads.Address +import org.session.libsession.utilities.GroupUtil + sealed class Destination { class Contact(val publicKey: String) : Destination() @@ -7,9 +11,19 @@ sealed class Destination { class OpenGroup(val channel: Long, val server: String) : Destination() companion object { - //TODO need to implement the equivalent to TSThread and then implement from(...) - fun from(threadID: String): Destination { - return Contact(threadID) // Fake for dev + fun from(address: Address): Destination { + if (address.isContact) { + return Contact(address.contactIdentifier()) + } else if (address.isClosedGroup) { + val groupID = address.contactIdentifier().toByteArray() + val groupPublicKey = GroupUtil.getDecodedGroupID(groupID) + return ClosedGroup(groupPublicKey) + } else if (address.isOpenGroup) { + val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(address.contactIdentifier())!! + return OpenGroup(openGroup.channel, openGroup.server) + } else { + throw Exception("TODO: Handle legacy closed groups.") + } } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt index 07bed84988..7298f9f68d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt @@ -1,7 +1,7 @@ package org.session.libsession.messaging.messages.control.unused import com.google.protobuf.ByteString -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.control.ControlMessage import org.session.libsignal.libsignal.IdentityKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey @@ -21,7 +21,7 @@ class SessionRequest() : ControlMessage() { if (proto.nullMessage == null) return null val preKeyBundleProto = proto.preKeyBundleMessage ?: return null var registrationID: Int = 0 - registrationID = Configuration.shared.storage.getOrGenerateRegistrationID() //TODO no implementation for getOrGenerateRegistrationID yet + registrationID = MessagingConfiguration.shared.storage.getOrGenerateRegistrationID() //TODO no implementation for getOrGenerateRegistrationID yet //TODO just confirm if the above code does the equivalent to swift below: /*iOS code: Configuration.shared.storage.with { transaction in registrationID = Configuration.shared.storage.getOrGenerateRegistrationID(using: transaction) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt index 14a7735ac2..246474b9a3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt @@ -1,8 +1,6 @@ package org.session.libsession.messaging.messages.visible -import android.content.Context -import org.session.libsession.database.MessageDataProvider -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos @@ -46,7 +44,7 @@ class LinkPreview() { title?.let { linkPreviewProto.title = title } val attachmentID = attachmentID attachmentID?.let { - val attachmentProto = Configuration.shared.messageDataProvider.getAttachment(attachmentID) + val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachment(attachmentID) attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() } } // Build diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index b1d67c398b..3914072c36 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages.visible import com.goterl.lazycode.lazysodium.BuildConfig import org.session.libsession.database.MessageDataProvider -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos @@ -48,7 +48,7 @@ class Quote() { quoteProto.id = timestamp quoteProto.author = publicKey text?.let { quoteProto.text = text } - addAttachmentsIfNeeded(quoteProto, Configuration.shared.messageDataProvider) + addAttachmentsIfNeeded(quoteProto, MessagingConfiguration.shared.messageDataProvider) // Build try { return quoteProto.build() diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index be5a0d3061..a1344754e0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -2,8 +2,7 @@ package org.session.libsession.messaging.messages.visible import com.goterl.lazycode.lazysodium.BuildConfig -import org.session.libsession.database.MessageDataProvider -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.Message import org.session.libsignal.libsignal.logging.Log @@ -52,7 +51,7 @@ class VisibleMessage : Message() { return false } - fun toProto(): SignalServiceProtos.Content? { + override fun toProto(): SignalServiceProtos.Content? { val proto = SignalServiceProtos.Content.newBuilder() var attachmentIDs = this.attachmentIDs val dataMessage: SignalServiceProtos.DataMessage.Builder @@ -91,7 +90,7 @@ class VisibleMessage : Message() { } } //Attachments - val attachments = attachmentIDs.mapNotNull { Configuration.shared.messageDataProvider.getAttachment(it) } + val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getAttachment(it) } if (!attachments.all { it.isUploaded }) { if (BuildConfig.DEBUG) { //TODO equivalent to iOS's preconditionFailure diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt index 0cff14e41c..3c700ef719 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt @@ -5,11 +5,10 @@ import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.then -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.utilities.DotNetAPI import org.session.libsession.messaging.fileserver.FileServerAPI -import org.session.libsession.snode.SnodeAPI import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.util.Base64 @@ -56,7 +55,7 @@ object OpenGroupAPI: DotNetAPI() { // region Public API public fun getMessages(channel: Long, server: String): Promise, Exception> { Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.") - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage val parameters = mutableMapOf( "include_annotations" to 1 ) val lastMessageServerID = storage.getLastMessageServerID(channel, server) if (lastMessageServerID != null) { @@ -161,7 +160,7 @@ object OpenGroupAPI: DotNetAPI() { public fun getDeletedMessageServerIDs(channel: Long, server: String): Promise, Exception> { Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.") - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage val parameters = mutableMapOf() val lastDeletionServerID = storage.getLastDeletionServerID(channel, server) if (lastDeletionServerID != null) { @@ -193,7 +192,7 @@ object OpenGroupAPI: DotNetAPI() { public fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise { val deferred = deferred() - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage val userKeyPair = storage.getUserKeyPair() ?: throw Error.Generic val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic Thread { @@ -287,7 +286,7 @@ object OpenGroupAPI: DotNetAPI() { val memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt() val profilePictureURL = info["avatar"] as String val publicChatInfo = OpenGroupInfo(displayName, profilePictureURL, memberCount) - Configuration.shared.storage.setUserCount(channel, server, memberCount) + MessagingConfiguration.shared.storage.setUserCount(channel, server, memberCount) publicChatInfo } catch (exception: Exception) { Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.") @@ -298,7 +297,7 @@ object OpenGroupAPI: DotNetAPI() { } public fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) { - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage storage.setUserCount(channel, server, info.memberCount) storage.updateTitle(groupID, info.displayName) // Download and update profile picture if needed diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt index 6472a9f1b1..1b5b274549 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt @@ -1,6 +1,6 @@ package org.session.libsession.messaging.opengroups -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.util.Hex @@ -24,7 +24,7 @@ public data class OpenGroupMessage( // region Settings companion object { fun from(message: VisibleMessage, server: String): OpenGroupMessage? { - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey() ?: return null // Validation if (!message.isValid()) { return null } // Should be valid at this point diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index 43dfa79187..27c548756a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -1,6 +1,6 @@ package org.session.libsession.messaging.sending_receiving -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ClosedGroupUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate @@ -39,7 +39,7 @@ object MessageReceiver { } internal fun parse(data: ByteArray, openGroupServerID: Long?): Pair { - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey() val isOpenGroupMessage = openGroupServerID != null // Parse the envelope diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt index 264d8c4cc2..a52f696c3e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt @@ -1,6 +1,6 @@ package org.session.libsession.messaging.sending_receiving -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error import org.session.libsession.utilities.AESGCM @@ -21,26 +21,26 @@ import javax.crypto.spec.SecretKeySpec object MessageReceiverDecryption { internal fun decryptWithSignalProtocol(envelope: SignalServiceProtos.Envelope): Pair { - val storage = Configuration.shared.signalStorage - val sskDatabase = Configuration.shared.sskDatabase - val sessionResetImp = Configuration.shared.sessionResetImp - val certificateValidator = Configuration.shared.certificateValidator + val storage = MessagingConfiguration.shared.signalStorage + val sskDatabase = MessagingConfiguration.shared.sskDatabase + val sessionResetImp = MessagingConfiguration.shared.sessionResetImp + val certificateValidator = MessagingConfiguration.shared.certificateValidator val data = envelope.content if (data.count() == 0) { throw Error.NoData } - val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey + val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey val localAddress = SignalServiceAddress(userPublicKey) val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator) val result = cipher.decrypt(SignalServiceEnvelope(envelope)) - return Pair(result, result.sender) + return Pair(ByteArray(1), result.sender) // TODO: Return real plaintext } internal fun decryptWithSharedSenderKeys(envelope: SignalServiceProtos.Envelope): Pair { // 1. ) Check preconditions val groupPublicKey = envelope.source - if (!Configuration.shared.storage.isClosedGroup(groupPublicKey)) { throw Error.InvalidGroupPublicKey } + if (!MessagingConfiguration.shared.storage.isClosedGroup(groupPublicKey)) { throw Error.InvalidGroupPublicKey } val data = envelope.content if (data.count() == 0) { throw Error.NoData } - val groupPrivateKey = Configuration.shared.storage.getClosedGroupPrivateKey(groupPublicKey) ?: throw Error.NoGroupPrivateKey + val groupPrivateKey = MessagingConfiguration.shared.storage.getClosedGroupPrivateKey(groupPublicKey) ?: throw Error.NoGroupPrivateKey // 2. ) Parse the wrapper val wrapper = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data) val ivAndCiphertext = wrapper.ciphertext.toByteArray() @@ -54,7 +54,7 @@ object MessageReceiverDecryption { // 4. ) Parse the closed group ciphertext message val closedGroupCiphertextMessage = ClosedGroupCiphertextMessage.from(closedGroupCiphertextMessageAsData) ?: throw Error.ParsingFailed val senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString() - if (senderPublicKey == Configuration.shared.storage.getUserPublicKey()) { throw Error.SelfSend } + if (senderPublicKey == MessagingConfiguration.shared.storage.getUserPublicKey()) { throw Error.SelfSend } // 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content val plaintext = SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, groupPublicKey, senderPublicKey, closedGroupCiphertextMessage.keyIndex) // 6. ) Return diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt index 54f70d8590..1f767babdb 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -1,6 +1,6 @@ package org.session.libsession.messaging.sending_receiving -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ClosedGroupUpdate @@ -9,9 +9,9 @@ import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI -import org.session.libsession.utilities.LKGroupUtilities +import org.session.libsession.messaging.threads.Address +import org.session.libsession.utilities.GroupUtil import org.session.libsignal.libsignal.util.Hex -import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet @@ -37,7 +37,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, } private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) { - + // TODO } private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) { @@ -89,8 +89,8 @@ private fun MessageReceiver.handleClosedGroupUpdate(message: ClosedGroupUpdate) } private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) { - val storage = Configuration.shared.storage - val sskDatabase = Configuration.shared.sskDatabase + val storage = MessagingConfiguration.shared.storage + val sskDatabase = MessagingConfiguration.shared.sskDatabase val kind = message.kind!! as ClosedGroupUpdate.Kind.New val groupPublicKey = kind.groupPublicKey.toHexString() val name = kind.name @@ -122,27 +122,24 @@ private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) { MessageSender.requestSenderKey(groupPublicKey, publicKey) } // Create the group - val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) - val groupDB = DatabaseFactory.getGroupDatabase(context) - if (groupDB.getGroup(groupID).orNull() != null) { + val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey) + if (storage.getGroup(groupID) != null) { // Update the group - groupDB.updateTitle(groupID, name) - groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + storage.updateTitle(groupID, name) + storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) } else { - groupDB.create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), - null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) + storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), + null, null, LinkedList(admins.map { Address.fromSerialized(it) })) } - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) + storage.setProfileSharing(Address.fromSerialized(groupID), true) // Add the group to the user's set of public keys to poll for sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) // Notify the PN server - PushNotificationAPI.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) + PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) // Notify the user + /* TODO insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) - // Establish sessions if needed - establishSessionsWithMembersIfNeeded(context, members) - - + */ } private fun MessageReceiver.handleGroupUpdate(message: ClosedGroupUpdate) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 1d18f3e9df..21f7893c2f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -1,10 +1,9 @@ package org.session.libsession.messaging.sending_receiving -import com.google.protobuf.MessageOrBuilder import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message @@ -64,7 +63,7 @@ object MessageSender { fun sendToSnodeDestination(destination: Destination, message: Message): Promise { val deferred = deferred() val promise = deferred.promise - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage val preconditionFailure = Exception("Destination should not be open groups!") var snodeMessage: SnodeMessage? = null message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */ @@ -152,7 +151,7 @@ object MessageSender { fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise { val deferred = deferred() val promise = deferred.promise - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage val preconditionFailure = Exception("Destination should not be contacts or closed groups!") message.sentTimestamp = System.currentTimeMillis() message.sender = storage.getUserPublicKey() diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt index 3aa4cbb0fd..8604c3d49c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt @@ -2,16 +2,17 @@ package org.session.libsession.messaging.sending_receiving -import android.content.Context import android.util.Log import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred -import org.session.libsession.messaging.Configuration -import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.control.ClosedGroupUpdate import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI -import org.session.libsession.utilities.LKGroupUtilities +import org.session.libsession.messaging.sending_receiving.MessageSender.Error +import org.session.libsession.messaging.threads.Address +import org.session.libsession.utilities.GroupUtil + import org.session.libsignal.libsignal.ecc.Curve import org.session.libsignal.libsignal.util.Hex @@ -26,8 +27,9 @@ import java.util.* fun MessageSender.createClosedGroup(name: String, members: Collection): Promise { val deferred = deferred() // Prepare + val storage = MessagingConfiguration.shared.storage val members = members - val userPublicKey = Configuration.shared.storage.getUserPublicKey()!! + val userPublicKey = storage.getUserPublicKey()!! // Generate a key pair for the group val groupKeyPair = Curve.generateKeyPair() val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix @@ -41,12 +43,9 @@ fun MessageSender.createClosedGroup(name: String, members: Collection): // Create the group val admins = setOf( userPublicKey ) val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) - /* TODO: - DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), - null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) - */ + val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey) + storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), null, null, LinkedList(admins.map { Address.fromSerialized(it) })) + storage.setProfileSharing(Address.fromSerialized(groupID), true) // Send a closed group update message to all members using established channels val promises = mutableListOf>() for (member in members) { @@ -55,16 +54,17 @@ fun MessageSender.createClosedGroup(name: String, members: Collection): senderKeys, membersAsData, adminsAsData) val closedGroupUpdate = ClosedGroupUpdate() closedGroupUpdate.kind = closedGroupUpdateKind - val promise = MessageSender.sendNonDurably(closedGroupUpdate, threadID) + val address = Address.fromSerialized(member) + val promise = MessageSender.sendNonDurably(closedGroupUpdate, address) promises.add(promise) } // Add the group to the user's set of public keys to poll for - Configuration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) + MessagingConfiguration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) // Notify the PN server PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) // Notify the user + val threadID =storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) /* TODO - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) */ // Fulfill the promise @@ -75,46 +75,47 @@ fun MessageSender.createClosedGroup(name: String, members: Collection): fun MessageSender.update(groupPublicKey: String, members: Collection, name: String): Promise { val deferred = deferred() - val userPublicKey = Configuration.shared.storage.getUserPublicKey()!! - val sskDatabase = Configuration.shared.sskDatabase - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() + val storage = MessagingConfiguration.shared.storage + val userPublicKey = storage.getUserPublicKey()!! + val sskDatabase = MessagingConfiguration.shared.sskDatabase + val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey) + val group = storage.getGroup(groupID) if (group == null) { Log.d("Loki", "Can't update nonexistent closed group.") - return deferred.reject(Error.NoThread) + deferred.reject(Error.NoThread) + return deferred.promise } val oldMembers = group.members.map { it.serialize() }.toSet() val newMembers = members.minus(oldMembers) val membersAsData = members.map { Hex.fromStringCondensed(it) } val admins = group.admins.map { it.serialize() } val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) + val groupPrivateKey = sskDatabase.getClosedGroupPrivateKey(groupPublicKey) if (groupPrivateKey == null) { Log.d("Loki", "Couldn't get private key for closed group.") - return@Thread deferred.reject(Error.NoPrivateKey) + deferred.reject(Error.NoPrivateKey) + return deferred.promise } val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet() val removedMembers = oldMembers.minus(members) val isUserLeaving = removedMembers.contains(userPublicKey) - var newSenderKeys = listOf() + val newSenderKeys: List if (wasAnyUserRemoved) { if (isUserLeaving && removedMembers.count() != 1) { Log.d("Loki", "Can't remove self and others simultaneously.") - return@Thread deferred.reject(Error.InvalidUpdate) + deferred.reject(Error.InvalidClosedGroupUpdate) + return deferred.promise } - // Establish sessions if needed - establishSessionsWithMembersIfNeeded(context, members) // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually) - for (member in oldMembers) { - @Suppress("NAME_SHADOWING") - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), + val promises = oldMembers.map { member -> + val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, setOf(), membersAsData, adminsAsData) - @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) - job.setContext(context) - job.onRun() // Run the job immediately + val closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + val address = Address.fromSerialized(member) + MessageSender.sendNonDurably(closedGroupUpdate, address).get() } + val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) for (pair in allOldRatchets) { val senderPublicKey = pair.first @@ -128,30 +129,30 @@ fun MessageSender.update(groupPublicKey: String, members: Collection, na // send it out to all members (minus the removed ones) using established channels. if (isUserLeaving) { sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) - groupDB.setActive(groupID, false) - groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey)) + storage.setActive(groupID, false) + storage.removeMember(groupID, Address.fromSerialized(userPublicKey)) // Notify the PN server - LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) + PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) } else { // Send closed group update messages to any new members using established channels for (member in newMembers) { - @Suppress("NAME_SHADOWING") - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, + val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData) - @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) + val closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + val address = Address.fromSerialized(member) + MessageSender.sendNonDurably(closedGroupUpdate, address) } // Send out the user's new ratchet to all members (minus the removed ones) using established channels val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) for (member in members) { if (member == userPublicKey) { continue } - @Suppress("NAME_SHADOWING") - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) - @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) + val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + val closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + val address = Address.fromSerialized(member) + MessageSender.sendNonDurably(closedGroupUpdate, address) } } } else if (newMembers.isNotEmpty()) { @@ -161,49 +162,68 @@ fun MessageSender.update(groupPublicKey: String, members: Collection, na ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) } // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, + val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, newSenderKeys, membersAsData, adminsAsData) - val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) - // Establish sessions if needed - establishSessionsWithMembersIfNeeded(context, newMembers) + val closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + val address = Address.fromSerialized(groupID) + MessageSender.send(closedGroupUpdate, address) // Send closed group update messages to the new members using established channels var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current) allSenderKeys = allSenderKeys.union(newSenderKeys) for (member in newMembers) { - @Suppress("NAME_SHADOWING") - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, + val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) - @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) + val closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + val address = Address.fromSerialized(member) + MessageSender.send(closedGroupUpdate, address) } } else { val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current) - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, + val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, allSenderKeys, membersAsData, adminsAsData) - val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) - ApplicationContext.getInstance(context).jobManager.add(job) + val closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + val address = Address.fromSerialized(groupID) + MessageSender.send(closedGroupUpdate, address) } // Update the group - groupDB.updateTitle(groupID, name) + storage.updateTitle(groupID, name) if (!isUserLeaving) { // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead - groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) } // Notify the user val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + /* TODO insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID) + */ deferred.resolve(Unit) return deferred.promise } +fun MessageSender.leave(groupPublicKey: String) { + val storage = MessagingConfiguration.shared.storage + val userPublicKey = storage.getUserPublicKey()!! + val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey) + val group = storage.getGroup(groupID) + if (group == null) { + Log.d("Loki", "Can't leave nonexistent closed group.") + return + } + val name = group.title + val oldMembers = group.members.map { it.serialize() }.toSet() + val newMembers = oldMembers.minus(userPublicKey) + return update(groupPublicKey, newMembers, name).get() +} + fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) { Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.") - // Send the request + val address = Address.fromSerialized(senderPublicKey) val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey)) val closedGroupUpdate = ClosedGroupUpdate() closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, Destination.ClosedGroup(groupPublicKey)) + MessageSender.send(closedGroupUpdate, address) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt index de5b6fa89e..4efecc8d15 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt @@ -1,35 +1,39 @@ package org.session.libsession.messaging.sending_receiving import nl.komponents.kovenant.Promise +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.threads.Address import org.session.libsignal.service.api.messages.SignalServiceAttachment -fun MessageSender.send(message: VisibleMessage, attachments: List, threadID: String) { +fun MessageSender.send(message: VisibleMessage, attachments: List, address: Address) { prep(attachments, message) - send(message, threadID) + send(message, address) } -fun MessageSender.send(message: Message, threadID: String) { +fun MessageSender.send(message: Message, address: Address) { + val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address) message.threadID = threadID - val destination = Destination.from(threadID) + val destination = Destination.from(address) val job = MessageSendJob(message, destination) JobQueue.shared.add(job) } -fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List, threadID: String): Promise { +fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List, address: Address): Promise { prep(attachments, message) // TODO: Deal with attachments - return sendNonDurably(message, threadID) + return sendNonDurably(message, address) } -fun MessageSender.sendNonDurably(message: Message, threadID: String): Promise { +fun MessageSender.sendNonDurably(message: Message, address: Address): Promise { + val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address) message.threadID = threadID - val destination = Destination.from(threadID) + val destination = Destination.from(address) return MessageSender.send(message, destination) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt index f1c9dd2c88..393f24de12 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt @@ -1,7 +1,7 @@ package org.session.libsession.messaging.sending_receiving import com.google.protobuf.ByteString -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.utilities.UnidentifiedAccessUtil @@ -21,11 +21,11 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded object MessageSenderEncryption { internal fun encryptWithSignalProtocol(plaintext: ByteArray, message: Message, recipientPublicKey: String): ByteArray{ - val storage = Configuration.shared.signalStorage - val sskDatabase = Configuration.shared.sskDatabase - val sessionResetImp = Configuration.shared.sessionResetImp + val storage = MessagingConfiguration.shared.signalStorage + val sskDatabase = MessagingConfiguration.shared.sskDatabase + val sessionResetImp = MessagingConfiguration.shared.sessionResetImp val localAddress = SignalServiceAddress(recipientPublicKey) - val certificateValidator = Configuration.shared.certificateValidator + val certificateValidator = MessagingConfiguration.shared.certificateValidator val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator) val signalProtocolAddress = SignalProtocolAddress(recipientPublicKey, 1) val unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(recipientPublicKey) @@ -36,7 +36,7 @@ object MessageSenderEncryption { internal fun encryptWithSharedSenderKeys(plaintext: ByteArray, groupPublicKey: String): ByteArray { // 1. ) Encrypt the data with the user's sender key - val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey + val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey val ciphertextAndKeyIndex = SharedSenderKeysImplementation.shared.encrypt(plaintext, groupPublicKey, userPublicKey) val ivAndCiphertext = ciphertextAndKeyIndex.first val keyIndex = ciphertextAndKeyIndex.second diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index 457b83ad88..dd1d73e04e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -1,16 +1,16 @@ package org.session.libsession.messaging.sending_receiving.notifications -import android.content.Context import nl.komponents.kovenant.functional.map import okhttp3.* -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.util.JsonUtil import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI import org.session.libsignal.service.loki.utilities.retryIfNeeded -import java.io.IOException object PushNotificationAPI { + val context = MessagingConfiguration.shared.context val server = "https://live.apns.getsession.org" val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" private val maxRetryCount = 4 @@ -46,8 +46,8 @@ object PushNotificationAPI { } } // Unsubscribe from all closed groups - val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys() - val userPublicKey = Configuration.shared.storage.getUserPublicKey()!! + val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys() + val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()!! allClosedGroupPublicKeys.forEach { closedGroup -> performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey) } @@ -76,7 +76,7 @@ object PushNotificationAPI { } } // Subscribe to all closed groups - val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys() + val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys() allClosedGroupPublicKeys.forEach { closedGroup -> performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt new file mode 100644 index 0000000000..8d990776f3 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt @@ -0,0 +1,90 @@ +package org.session.libsession.messaging.sending_receiving.pollers + +import android.os.Handler +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map + +import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveJob +import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.utilities.successBackground + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.loki.utilities.getRandomElementOrNull + +class ClosedGroupPoller { + private var isPolling = false + private val handler: Handler by lazy { Handler() } + + private val task = object : Runnable { + + override fun run() { + poll() + handler.postDelayed(this, ClosedGroupPoller.pollInterval) + } + } + + // region Settings + companion object { + private val pollInterval: Long = 2 * 1000 + } + // endregion + + // region Error + class InsufficientSnodesException() : Exception("No snodes left to poll.") + class PollingCanceledException() : Exception("Polling canceled.") + // endregion + + // region Public API + public fun startIfNeeded() { + if (isPolling) { return } + isPolling = true + task.run() + } + + public fun pollOnce(): List> { + if (isPolling) { return listOf() } + isPolling = true + return poll() + } + + public fun stopIfNeeded() { + isPolling = false + handler.removeCallbacks(task) + } + // endregion + + // region Private API + private fun poll(): List> { + if (!isPolling) { return listOf() } + val publicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys() + return publicKeys.map { publicKey -> + val promise = SnodeAPI.getSwarm(publicKey).bind { swarm -> + val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure + if (!isPolling) { throw PollingCanceledException() } + SnodeAPI.getRawMessages(snode, publicKey).map {SnodeAPI.parseRawMessagesResponse(it, snode, publicKey) } + } + promise.successBackground { messages -> + if (messages.isNotEmpty()) { + Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.") + } + messages.forEach { message -> + val rawMessageAsJSON = message as? Map<*, *> + val base64EncodedData = rawMessageAsJSON?.get("data") as? String + val data = base64EncodedData?.let { Base64.decode(it) } ?: return@forEach + val job = MessageReceiveJob(MessageWrapper.unwrap(data), false) + JobQueue.shared.add(job) + } + } + promise.fail { + Log.d("Loki", "Polling failed for closed group with public key: $publicKey due to error: $it.") + } + promise.map { Unit } + } + } + // endregion +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt new file mode 100644 index 0000000000..1c541cb2e2 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -0,0 +1,307 @@ +package org.session.libsession.messaging.sending_receiving.pollers + +import android.content.Context +import android.os.Handler +import org.thoughtcrime.securesms.logging.Log +import androidx.annotation.WorkerThread +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.jobs.PushDecryptJob +import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob +import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol +import org.thoughtcrime.securesms.loki.utilities.successBackground +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.session.libsignal.libsignal.util.guava.Optional +import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer +import org.session.libsignal.service.api.messages.SignalServiceContent +import org.session.libsignal.service.api.messages.SignalServiceDataMessage +import org.session.libsignal.service.api.messages.SignalServiceGroup +import org.session.libsignal.service.api.messages.multidevice.SentTranscriptMessage +import org.session.libsignal.service.api.push.SignalServiceAddress +import org.session.libsignal.service.loki.api.fileserver.FileServerAPI +import org.session.libsignal.service.loki.api.opengroups.PublicChat +import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI +import org.session.libsignal.service.loki.api.opengroups.PublicChatMessage +import org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol +import java.security.MessageDigest +import java.util.* +import java.util.concurrent.CompletableFuture + +class OpenGroupPoller(private val context: Context, private val group: PublicChat) { + private val handler by lazy { Handler() } + private var hasStarted = false + private var isPollOngoing = false + public var isCaughtUp = false + + // region Convenience + private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) + private var displayNameUpdatees = setOf() + + private val api: PublicChatAPI + get() = { + val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() + val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context) + val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context) + val openGroupDatabase = DatabaseFactory.getGroupDatabase(context) + PublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase, openGroupDatabase) + }() + // endregion + + // region Tasks + private val pollForNewMessagesTask = object : Runnable { + + override fun run() { + pollForNewMessages() + handler.postDelayed(this, pollForNewMessagesInterval) + } + } + + private val pollForDeletedMessagesTask = object : Runnable { + + override fun run() { + pollForDeletedMessages() + handler.postDelayed(this, pollForDeletedMessagesInterval) + } + } + + private val pollForModeratorsTask = object : Runnable { + + override fun run() { + pollForModerators() + handler.postDelayed(this, pollForModeratorsInterval) + } + } + + private val pollForDisplayNamesTask = object : Runnable { + + override fun run() { + pollForDisplayNames() + handler.postDelayed(this, pollForDisplayNamesInterval) + } + } + // endregion + + // region Settings + companion object { + private val pollForNewMessagesInterval: Long = 4 * 1000 + private val pollForDeletedMessagesInterval: Long = 60 * 1000 + private val pollForModeratorsInterval: Long = 10 * 60 * 1000 + private val pollForDisplayNamesInterval: Long = 60 * 1000 + } + // endregion + + // region Lifecycle + fun startIfNeeded() { + if (hasStarted) return + pollForNewMessagesTask.run() + pollForDeletedMessagesTask.run() + pollForModeratorsTask.run() + pollForDisplayNamesTask.run() + hasStarted = true + } + + fun stop() { + handler.removeCallbacks(pollForNewMessagesTask) + handler.removeCallbacks(pollForDeletedMessagesTask) + handler.removeCallbacks(pollForModeratorsTask) + handler.removeCallbacks(pollForDisplayNamesTask) + hasStarted = false + } + // endregion + + // region Polling + private fun getDataMessage(message: PublicChatMessage): SignalServiceDataMessage { + val id = group.id.toByteArray() + val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null, null) + val quote = if (message.quote != null) { + SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteePublicKey), message.quote!!.quotedMessageBody, listOf()) + } else { + null + } + val attachments = message.attachments.mapNotNull { attachment -> + if (attachment.kind != PublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null } + SignalServiceAttachmentPointer( + attachment.serverID, + attachment.contentType, + ByteArray(0), + Optional.of(attachment.size), + Optional.absent(), + attachment.width, attachment.height, + Optional.absent(), + Optional.of(attachment.fileName), + false, + Optional.fromNullable(attachment.caption), + attachment.url) + } + val linkPreview = message.attachments.firstOrNull { it.kind == PublicChatMessage.Attachment.Kind.LinkPreview } + val signalLinkPreviews = mutableListOf() + if (linkPreview != null) { + val attachment = SignalServiceAttachmentPointer( + linkPreview.serverID, + linkPreview.contentType, + ByteArray(0), + Optional.of(linkPreview.size), + Optional.absent(), + linkPreview.width, linkPreview.height, + Optional.absent(), + Optional.of(linkPreview.fileName), + false, + Optional.fromNullable(linkPreview.caption), + linkPreview.url) + signalLinkPreviews.add(SignalServiceDataMessage.Preview(linkPreview.linkPreviewURL!!, linkPreview.linkPreviewTitle!!, Optional.of(attachment))) + } + val body = if (message.body == message.timestamp.toString()) "" else message.body // Workaround for the fact that the back-end doesn't accept messages without a body + return SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, false, 0, false, null, false, quote, null, signalLinkPreviews, null) + } + + fun pollForNewMessages(): Promise { + fun processIncomingMessage(message: PublicChatMessage) { + // If the sender of the current message is not a slave device, set the display name in the database + val masterHexEncodedPublicKey = MultiDeviceProtocol.shared.getMasterDevice(message.senderPublicKey) + if (masterHexEncodedPublicKey == null) { + val senderDisplayName = "${message.displayName} (...${message.senderPublicKey.takeLast(8)})" + DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.senderPublicKey, senderDisplayName) + } + val senderHexEncodedPublicKey = masterHexEncodedPublicKey ?: message.senderPublicKey + val serviceDataMessage = getDataMessage(message) + val serviceContent = SignalServiceContent(serviceDataMessage, senderHexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.serverTimestamp, false, false) + if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) { + PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID)) + } else { + PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID)) + } + // Update profile picture if needed + val senderAsRecipient = Recipient.from(context, Address.fromSerialized(senderHexEncodedPublicKey), false) + if (message.profilePicture != null && message.profilePicture!!.url.isNotEmpty()) { + val profileKey = message.profilePicture!!.profileKey + val url = message.profilePicture!!.url + if (senderAsRecipient.profileKey == null || !MessageDigest.isEqual(senderAsRecipient.profileKey, profileKey)) { + val database = DatabaseFactory.getRecipientDatabase(context) + database.setProfileKey(senderAsRecipient, profileKey) + ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderAsRecipient, url)) + } + } + } + fun processOutgoingMessage(message: PublicChatMessage) { + val messageServerID = message.serverID ?: return + val messageID = DatabaseFactory.getLokiMessageDatabase(context).getMessageID(messageServerID) + var isDuplicate = false + if (messageID != null) { + isDuplicate = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageID) >= 0 + || DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID) >= 0 + } + if (isDuplicate) { return } + if (message.body.isEmpty() && message.attachments.isEmpty() && message.quote == null) { return } + val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) + val dataMessage = getDataMessage(message) + SessionMetaProtocol.dropFromTimestampCacheIfNeeded(message.serverTimestamp) + val transcript = SentTranscriptMessage(userHexEncodedPublicKey, message.serverTimestamp, dataMessage, dataMessage.expiresInSeconds.toLong(), Collections.singletonMap(userHexEncodedPublicKey, false)) + transcript.messageServerID = messageServerID + if (dataMessage.quote.isPresent || (dataMessage.attachments.isPresent && dataMessage.attachments.get().size > 0) || dataMessage.previews.isPresent) { + PushDecryptJob(context).handleSynchronizeSentMediaMessage(transcript) + } else { + PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript) + } + // If we got a message from our master device then make sure our mapping stays in sync + val recipient = Recipient.from(context, Address.fromSerialized(message.senderPublicKey), false) + if (recipient.isUserMasterDevice && message.profilePicture != null) { + val profileKey = message.profilePicture!!.profileKey + val url = message.profilePicture!!.url + if (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profileKey)) { + val database = DatabaseFactory.getRecipientDatabase(context) + database.setProfileKey(recipient, profileKey) + database.setProfileAvatar(recipient, url) + ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded() + } + } + } + if (isPollOngoing) { return Promise.of(Unit) } + isPollOngoing = true + val userDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userHexEncodedPublicKey) + var uniqueDevices = setOf() + val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + FileServerAPI.configure(userHexEncodedPublicKey, userPrivateKey, apiDB) + // Kovenant propagates a context to chained promises, so LokiPublicChatAPI.sharedContext should be used for all of the below + val promise = api.getMessages(group.channel, group.server).bind(PublicChatAPI.sharedContext) { messages -> + /* + if (messages.isNotEmpty()) { + // We need to fetch the device mapping for any devices we don't have + uniqueDevices = messages.map { it.senderPublicKey }.toSet() + val devicesToUpdate = uniqueDevices.filter { !userDevices.contains(it) && FileServerAPI.shared.hasDeviceLinkCacheExpired(publicKey = it) } + if (devicesToUpdate.isNotEmpty()) { + return@bind FileServerAPI.shared.getDeviceLinks(devicesToUpdate.toSet()).then { messages } + } + } + */ + Promise.of(messages) + } + promise.successBackground { + /* + val newDisplayNameUpdatees = uniqueDevices.mapNotNull { + // This will return null if the current device is a master device + MultiDeviceProtocol.shared.getMasterDevice(it) + }.toSet() + // Fetch the display names of the master devices + displayNameUpdatees = displayNameUpdatees.union(newDisplayNameUpdatees) + */ + } + promise.successBackground { messages -> + // Process messages in the background + messages.forEach { message -> + if (userDevices.contains(message.senderPublicKey)) { + processOutgoingMessage(message) + } else { + processIncomingMessage(message) + } + } + isCaughtUp = true + isPollOngoing = false + } + promise.fail { + Log.d("Loki", "Failed to get messages for group chat with ID: ${group.channel} on server: ${group.server}.") + isPollOngoing = false + } + return promise.map { Unit } + } + + private fun pollForDisplayNames() { + if (displayNameUpdatees.isEmpty()) { return } + val hexEncodedPublicKeys = displayNameUpdatees + displayNameUpdatees = setOf() + api.getDisplayNames(hexEncodedPublicKeys, group.server).successBackground { mapping -> + for (pair in mapping.entries) { + val senderDisplayName = "${pair.value} (...${pair.key.takeLast(8)})" + DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, pair.key, senderDisplayName) + } + }.fail { + displayNameUpdatees = displayNameUpdatees.union(hexEncodedPublicKeys) + } + } + + private fun pollForDeletedMessages() { + api.getDeletedMessageServerIDs(group.channel, group.server).success { deletedMessageServerIDs -> + val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context) + val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { lokiMessageDatabase.getMessageID(it) } + val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context) + val mmsMessageDatabase = DatabaseFactory.getMmsDatabase(context) + deletedMessageIDs.forEach { + smsMessageDatabase.deleteMessage(it) + mmsMessageDatabase.delete(it) + } + }.fail { + Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${group.channel} on server: ${group.server}.") + } + } + + private fun pollForModerators() { + api.getModerators(group.channel, group.server) + } + // endregion +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt new file mode 100644 index 0000000000..7afce79e85 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -0,0 +1,111 @@ +package org.session.libsession.messaging.sending_receiving.pollers + +import nl.komponents.kovenant.* +import nl.komponents.kovenant.functional.bind + +import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveJob +import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.snode.Snode +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeConfiguration + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.util.Base64 + +import java.security.SecureRandom +import java.util.* + +private class PromiseCanceledException : Exception("Promise canceled.") + +class Poller { + private val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: "" + private var hasStarted: Boolean = false + private val usedSnodes: MutableSet = mutableSetOf() + public var isCaughtUp = false + + // region Settings + companion object { + private val retryInterval: Long = 1 * 1000 + } + // endregion + + // region Public API + fun startIfNeeded() { + if (hasStarted) { return } + Log.d("Loki", "Started polling.") + hasStarted = true + setUpPolling() + } + + fun stopIfNeeded() { + Log.d("Loki", "Stopped polling.") + hasStarted = false + usedSnodes.clear() + } + // endregion + + // region Private API + private fun setUpPolling() { + if (!hasStarted) { return; } + val thread = Thread.currentThread() + SnodeAPI.getSwarm(userPublicKey).bind(SnodeAPI.messagePollingContext) { + usedSnodes.clear() + val deferred = deferred(SnodeAPI.messagePollingContext) + pollNextSnode(deferred) + deferred.promise + }.always { + Timer().schedule(object : TimerTask() { + + override fun run() { + thread.run { setUpPolling() } + } + }, retryInterval) + } + } + + private fun pollNextSnode(deferred: Deferred) { + val swarm = SnodeConfiguration.shared.storage.getSwarm(userPublicKey) ?: setOf() + val unusedSnodes = swarm.subtract(usedSnodes) + if (unusedSnodes.isNotEmpty()) { + val index = SecureRandom().nextInt(unusedSnodes.size) + val nextSnode = unusedSnodes.elementAt(index) + usedSnodes.add(nextSnode) + Log.d("Loki", "Polling $nextSnode.") + poll(nextSnode, deferred).fail { exception -> + if (exception is PromiseCanceledException) { + Log.d("Loki", "Polling $nextSnode canceled.") + } else { + Log.d("Loki", "Polling $nextSnode failed; dropping it and switching to next snode.") + SnodeAPI.dropSnodeFromSwarmIfNeeded(nextSnode, userPublicKey) + pollNextSnode(deferred) + } + } + } else { + isCaughtUp = true + deferred.resolve() + } + } + + private fun poll(snode: Snode, deferred: Deferred): Promise { + if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) } + return SnodeAPI.getRawMessages(snode, userPublicKey).bind(SnodeAPI.messagePollingContext) { rawResponse -> + isCaughtUp = true + if (deferred.promise.isDone()) { + task { Unit } // The long polling connection has been canceled; don't recurse + } else { + val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey) + messages.forEach { message -> + val rawMessageAsJSON = message as? Map<*, *> + val base64EncodedData = rawMessageAsJSON?.get("data") as? String + val data = base64EncodedData?.let { Base64.decode(it) } ?: return@forEach + val job = MessageReceiveJob(MessageWrapper.unwrap(data), false) + JobQueue.shared.add(job) + } + poll(snode, deferred) + } + } + } + // endregion +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt b/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt new file mode 100644 index 0000000000..a4e01330a3 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt @@ -0,0 +1,181 @@ +package org.session.libsession.messaging.threads + +import android.content.Context +import android.os.Parcel +import android.os.Parcelable +import android.util.Pair +import androidx.annotation.VisibleForTesting +import org.session.libsession.utilities.DelimiterUtil.escape +import org.session.libsession.utilities.DelimiterUtil.split +import org.session.libsession.utilities.DelimiterUtil.unescape +import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.NumberUtil.isValidEmail +import org.session.libsignal.libsignal.util.guava.Optional +import org.session.libsignal.service.internal.util.Util +import java.lang.AssertionError +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import java.util.regex.Matcher +import java.util.regex.Pattern + +class Address private constructor(address: String) : Parcelable, Comparable { + private val address: String = address.toLowerCase() + + constructor(`in`: Parcel) : this(`in`.readString()!!) {} + + val isGroup: Boolean + get() = GroupUtil.isEncodedGroup(address) + val isClosedGroup: Boolean + get() = GroupUtil.isClosedGroup(address) + val isOpenGroup: Boolean + get() = GroupUtil.isOpenGroup(address) + val isMmsGroup: Boolean + get() = GroupUtil.isMmsGroup(address) + val isContact: Boolean + get() = !isGroup + + fun contactIdentifier(): String { + if (!isContact && !isOpenGroup) { + if (isGroup) throw AssertionError("Not e164, is group") + throw AssertionError("Not e164, unknown") + } + return address + } + + override fun toString(): String { + return address + } + + fun serialize(): String { + return address + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return if (other == null || other !is Address) false else address == other.address + } + + override fun hashCode(): Int { + return address.hashCode() + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(address) + } + + override fun compareTo(other: Address?): Int { + return address.compareTo(other?.address!!) + } + + @VisibleForTesting + class ExternalAddressFormatter internal constructor(localCountryCode: String, countryCode: Boolean) { + private val localNumber: Optional + private val localCountryCode: String + private val ALPHA_PATTERN = Pattern.compile("[a-zA-Z]") + fun format(number: String?): String { + return number ?: "Unknown" + } + + private fun parseAreaCode(e164Number: String, countryCode: Int): String? { + when (countryCode) { + 1 -> return e164Number.substring(2, 5) + 55 -> return e164Number.substring(3, 5) + } + return null + } + + private fun applyAreaCodeRules(localNumber: Optional, testNumber: String): String { + if (!localNumber.isPresent || !localNumber.get().areaCode.isPresent) { + return testNumber + } + val matcher: Matcher + when (localNumber.get().countryCode) { + 1 -> { + matcher = US_NO_AREACODE.matcher(testNumber) + if (matcher.matches()) { + return localNumber.get().areaCode.toString() + matcher.group() + } + } + 55 -> { + matcher = BR_NO_AREACODE.matcher(testNumber) + if (matcher.matches()) { + return localNumber.get().areaCode.toString() + matcher.group() + } + } + } + return testNumber + } + + private class PhoneNumber internal constructor(val e164Number: String, val countryCode: Int, areaCode: String?) { + val areaCode: Optional + + init { + this.areaCode = Optional.fromNullable(areaCode) + } + } + + companion object { + private val TAG = ExternalAddressFormatter::class.java.simpleName + private val SHORT_COUNTRIES: HashSet = object : HashSet() { + init { + add("NU") + add("TK") + add("NC") + add("AC") + } + } + private val US_NO_AREACODE = Pattern.compile("^(\\d{7})$") + private val BR_NO_AREACODE = Pattern.compile("^(9?\\d{8})$") + } + + init { + localNumber = Optional.absent() + this.localCountryCode = localCountryCode + } + } + + companion object { + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(`in`: Parcel): Address { + return Address(`in`) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + val UNKNOWN = Address("Unknown") + private val TAG = Address::class.java.simpleName + private val cachedFormatter = AtomicReference>() + fun fromSerialized(serialized: String): Address { + return Address(serialized) + } + + fun fromExternal(context: Context, external: String?): Address { + return fromSerialized(external!!) + } + + fun fromSerializedList(serialized: String, delimiter: Char): List
{ + val escapedAddresses = split(serialized, delimiter) + val addresses: MutableList
= LinkedList() + for (escapedAddress in escapedAddresses) { + addresses.add(fromSerialized(unescape(escapedAddress, delimiter))) + } + return addresses + } + + fun toSerializedList(addresses: List
, delimiter: Char): String { + Collections.sort(addresses) + val escapedAddresses: MutableList = LinkedList() + for (address in addresses) { + escapedAddresses.add(escape(address.serialize(), delimiter)) + } + return Util.join(escapedAddresses, delimiter.toString() + "") + } + } + +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt b/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt new file mode 100644 index 0000000000..ce19c18db8 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt @@ -0,0 +1,36 @@ +package org.session.libsession.messaging.threads + +import android.text.TextUtils +import org.session.libsession.utilities.GroupUtil +import java.io.IOException +import java.util.* + +class GroupRecord( + val encodedId: String, val title: String, members: String?, val avatar: ByteArray, + val avatarId: Long, val avatarKey: ByteArray, val avatarContentType: String, + val relay: String, val isActive: Boolean, val avatarDigest: ByteArray, val isMms: Boolean, val url: String, admins: String?, +) { + var members: List
= LinkedList
() + var admins: List
= LinkedList
() + fun getId(): ByteArray { + return try { + GroupUtil.getDecodedGroupIDAsData(encodedId.toByteArray()) + } catch (ioe: IOException) { + throw AssertionError(ioe) + } + } + + val isOpenGroup: Boolean + get() = Address.fromSerialized(encodedId).isOpenGroup + val isClosedGroup: Boolean + get() = Address.fromSerialized(encodedId).isClosedGroup + + init { + if (!TextUtils.isEmpty(members)) { + this.members = Address.fromSerializedList(members!!, ',') + } + if (!TextUtils.isEmpty(admins)) { + this.admins = Address.fromSerializedList(admins!!, ',') + } + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt index 8f881e8ad1..bf95e0f66c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt @@ -9,7 +9,7 @@ import okhttp3.MultipartBody import okhttp3.Request import okhttp3.RequestBody -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI import org.session.libsession.messaging.fileserver.FileServerAPI @@ -57,7 +57,7 @@ open class DotNetAPI { public data class UploadResult(val id: Long, val url: String, val digest: ByteArray?) public fun getAuthToken(server: String): Promise { - val storage = Configuration.shared.storage + val storage = MessagingConfiguration.shared.storage val token = storage.getAuthToken(server) if (token != null) { return Promise.of(token) } // Avoid multiple token requests to the server by caching @@ -76,7 +76,7 @@ open class DotNetAPI { private fun requestNewAuthToken(server: String): Promise { Log.d("Loki", "Requesting auth token for server: $server.") - val userKeyPair = Configuration.shared.storage.getUserKeyPair() ?: throw Error.Generic + val userKeyPair = MessagingConfiguration.shared.storage.getUserKeyPair() ?: throw Error.Generic val parameters: Map = mapOf( "pubKey" to userKeyPair.hexEncodedPublicKey ) return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map(SnodeAPI.sharedContext) { json -> try { @@ -102,7 +102,7 @@ open class DotNetAPI { private fun submitAuthToken(token: String, server: String): Promise { Log.d("Loki", "Submitting auth token for server: $server.") - val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.Generic + val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: throw Error.Generic val parameters = mapOf( "pubKey" to userPublicKey, "token" to token ) return execute(HTTPVerb.POST, server, "loki/v1/submit_challenge", false, parameters, isJSONRequired = false).map { token } } @@ -141,7 +141,7 @@ open class DotNetAPI { if (exception is HTTP.HTTPRequestFailedException) { val statusCode = exception.statusCode if (statusCode == 401 || statusCode == 403) { - Configuration.shared.storage.setAuthToken(server, null) + MessagingConfiguration.shared.storage.setAuthToken(server, null) throw Error.TokenExpired } } @@ -256,7 +256,7 @@ open class DotNetAPI { if (exception is HTTP.HTTPRequestFailedException) { val statusCode = exception.statusCode if (statusCode == 401 || statusCode == 403) { - Configuration.shared.storage.setAuthToken(server, null) + MessagingConfiguration.shared.storage.setAuthToken(server, null) } throw NonSuccessfulResponseCodeException("Request returned with status code ${exception.statusCode}.") } diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt index de4e2db801..39558e2ce2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt @@ -3,7 +3,7 @@ package org.session.libsession.messaging.utilities import com.goterl.lazycode.lazysodium.LazySodiumAndroid import com.goterl.lazycode.lazysodium.SodiumAndroid -import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.MessagingConfiguration import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.metadata.SignalProtos @@ -37,12 +37,12 @@ object UnidentifiedAccessUtil { } private fun getTargetUnidentifiedAccessKey(recipientPublicKey: String): ByteArray? { - val theirProfileKey = Configuration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16) + val theirProfileKey = MessagingConfiguration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16) return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey) } private fun getSelfUnidentifiedAccessKey(): ByteArray? { - val userPublicKey = Configuration.shared.storage.getUserPublicKey() + val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() if (userPublicKey != null) { return sodium.randomBytesBuf(16) } @@ -50,7 +50,7 @@ object UnidentifiedAccessUtil { } private fun getUnidentifiedAccessCertificate(): ByteArray? { - val userPublicKey = Configuration.shared.storage.getUserPublicKey() + val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() if (userPublicKey != null) { val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build() return certificate.toByteArray() diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index 0b550936be..3e3bac9f1c 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -2,17 +2,13 @@ package org.session.libsession.snode -import nl.komponents.kovenant.Kovenant -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.deferred +import nl.komponents.kovenant.* import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map -import nl.komponents.kovenant.task import org.session.libsession.snode.utilities.getRandomElement import org.session.libsignal.libsignal.logging.Log -import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.util.Base64 import org.session.libsignal.service.loki.api.MessageWrapper import org.session.libsignal.service.loki.api.utilities.HTTP @@ -24,8 +20,8 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope import java.security.SecureRandom object SnodeAPI { - val database = Configuration.shared.storage - val broadcaster = Configuration.shared.broadcaster + val database = SnodeConfiguration.shared.storage + val broadcaster = SnodeConfiguration.shared.broadcaster val sharedContext = Kovenant.createContext("LokiAPISharedContext") val messageSendingContext = Kovenant.createContext("LokiAPIMessageSendingContext") val messagePollingContext = Kovenant.createContext("LokiAPIMessagePollingContext") @@ -245,14 +241,13 @@ object SnodeAPI { } } - private fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List { + fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<*> { val messages = rawResponse["messages"] as? List<*> return if (messages != null) { updateLastMessageHashValueIfPossible(snode, publicKey, messages) - val newRawMessages = removeDuplicates(publicKey, messages) - parseEnvelopes(newRawMessages) + removeDuplicates(publicKey, messages) } else { - listOf() + listOf>() } } @@ -284,25 +279,6 @@ object SnodeAPI { } } - private fun parseEnvelopes(rawMessages: List<*>): List { - return rawMessages.mapNotNull { rawMessage -> - val rawMessageAsJSON = rawMessage as? Map<*, *> - val base64EncodedData = rawMessageAsJSON?.get("data") as? String - val data = base64EncodedData?.let { Base64.decode(it) } - if (data != null) { - try { - MessageWrapper.unwrap(data) - } catch (e: Exception) { - Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") - null - } - } else { - Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.") - null - } - } - } - // Error Handling internal fun handleSnodeError(statusCode: Int, json: Map<*, *>?, snode: Snode, publicKey: String? = null): Exception? { fun handleBadSnode() { @@ -366,5 +342,5 @@ object SnodeAPI { // Type Aliases typealias RawResponse = Map<*, *> -typealias MessageListPromise = Promise, Exception> +typealias MessageListPromise = Promise, Exception> typealias RawResponsePromise = Promise diff --git a/libsession/src/main/java/org/session/libsession/snode/Configuration.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeConfiguration.kt similarity index 58% rename from libsession/src/main/java/org/session/libsession/snode/Configuration.kt rename to libsession/src/main/java/org/session/libsession/snode/SnodeConfiguration.kt index 756351d15a..ba2923cf4f 100644 --- a/libsession/src/main/java/org/session/libsession/snode/Configuration.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeConfiguration.kt @@ -2,13 +2,13 @@ package org.session.libsession.snode import org.session.libsignal.service.loki.utilities.Broadcaster -class Configuration(val storage: SnodeStorageProtocol, val broadcaster: Broadcaster) { +class SnodeConfiguration(val storage: SnodeStorageProtocol, val broadcaster: Broadcaster) { companion object { - lateinit var shared: Configuration + lateinit var shared: SnodeConfiguration fun configure(storage: SnodeStorageProtocol, broadcaster: Broadcaster) { if (Companion::shared.isInitialized) { return } - shared = Configuration(storage, broadcaster) + shared = SnodeConfiguration(storage, broadcaster) } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/DelimiterUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/DelimiterUtil.kt new file mode 100644 index 0000000000..5ee2e634e1 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/DelimiterUtil.kt @@ -0,0 +1,18 @@ +package org.session.libsession.utilities + +import java.util.regex.Pattern + +object DelimiterUtil { + fun escape(value: String, delimiter: Char): String { + return value.replace("" + delimiter, "\\" + delimiter) + } + + fun unescape(value: String, delimiter: Char): String { + return value.replace("\\" + delimiter, "" + delimiter) + } + + fun split(value: String, delimiter: Char): Array { + val regex = "(? Promise.successBackground(callback: (value: V) -> Unit): Promise { + Thread { + try { + callback(get()) + } catch (e: Exception) { + Log.d("Loki", "Failed to execute task in background: ${e.message}.") + } + }.start() + return this +} + +fun Promise.timeout(millis: Long): Promise { + if (this.isDone()) { return this; } + val deferred = deferred() + Thread { + Thread.sleep(millis) + if (!deferred.promise.isDone()) { + deferred.reject(TimeoutException("Promise timed out.")) + } + }.start() + this.success { + if (!deferred.promise.isDone()) { deferred.resolve(it) } + }.fail { + if (!deferred.promise.isDone()) { deferred.reject(it) } + } + return deferred.promise +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt new file mode 100644 index 0000000000..a0563ed337 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -0,0 +1,1290 @@ +package org.session.libsession.utilities + +import android.content.Context +import android.content.SharedPreferences +import android.hardware.Camera +import android.net.Uri +import android.os.Build +import android.preference.PreferenceManager.* +import android.provider.Settings +import androidx.annotation.ArrayRes +import androidx.core.app.NotificationCompat +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.libsignal.util.Medium +import org.session.libsignal.service.internal.util.Base64 +import java.io.IOException +import java.security.SecureRandom +import java.util.* + +object TextSecurePreferences { + private val TAG = TextSecurePreferences::class.simpleName + + const val IDENTITY_PREF = "pref_choose_identity" + const val CHANGE_PASSPHRASE_PREF = "pref_change_passphrase" + const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase" + const val THEME_PREF = "pref_theme" + const val LANGUAGE_PREF = "pref_language" + private const val MMSC_CUSTOM_HOST_PREF = "pref_apn_mmsc_custom_host" + const val MMSC_HOST_PREF = "pref_apn_mmsc_host" + private const val MMSC_CUSTOM_PROXY_PREF = "pref_apn_mms_custom_proxy" + const val MMSC_PROXY_HOST_PREF = "pref_apn_mms_proxy" + private const val MMSC_CUSTOM_PROXY_PORT_PREF = "pref_apn_mms_custom_proxy_port" + const val MMSC_PROXY_PORT_PREF = "pref_apn_mms_proxy_port" + private const val MMSC_CUSTOM_USERNAME_PREF = "pref_apn_mmsc_custom_username" + const val MMSC_USERNAME_PREF = "pref_apn_mmsc_username" + private const val MMSC_CUSTOM_PASSWORD_PREF = "pref_apn_mmsc_custom_password" + const val MMSC_PASSWORD_PREF = "pref_apn_mmsc_password" + const val THREAD_TRIM_LENGTH = "pref_trim_length" + const val THREAD_TRIM_NOW = "pref_trim_now" + const val ENABLE_MANUAL_MMS_PREF = "pref_enable_manual_mms" + + private const val LAST_VERSION_CODE_PREF = "last_version_code" + private const val LAST_EXPERIENCE_VERSION_PREF = "last_experience_version_code" + private const val EXPERIENCE_DISMISSED_PREF = "experience_dismissed" + const val RINGTONE_PREF = "pref_key_ringtone" + const val VIBRATE_PREF = "pref_key_vibrate" + private const val NOTIFICATION_PREF = "pref_key_enable_notifications" + const val LED_COLOR_PREF = "pref_led_color" + const val LED_BLINK_PREF = "pref_led_blink" + private const val LED_BLINK_PREF_CUSTOM = "pref_led_blink_custom" + const val ALL_MMS_PREF = "pref_all_mms" + const val ALL_SMS_PREF = "pref_all_sms" + const val PASSPHRASE_TIMEOUT_INTERVAL_PREF = "pref_timeout_interval" + const val PASSPHRASE_TIMEOUT_PREF = "pref_timeout_passphrase" + const val SCREEN_SECURITY_PREF = "pref_screen_security" + private const val ENTER_SENDS_PREF = "pref_enter_sends" + private const val ENTER_PRESENT_PREF = "pref_enter_key" + private const val SMS_DELIVERY_REPORT_PREF = "pref_delivery_report_sms" + const val MMS_USER_AGENT = "pref_mms_user_agent" + private const val MMS_CUSTOM_USER_AGENT = "pref_custom_mms_user_agent" + private const val THREAD_TRIM_ENABLED = "pref_trim_threads" + private const val LOCAL_NUMBER_PREF = "pref_local_number" + private const val VERIFYING_STATE_PREF = "pref_verifying" + const val REGISTERED_GCM_PREF = "pref_gcm_registered" + private const val GCM_PASSWORD_PREF = "pref_gcm_password" + private const val SEEN_WELCOME_SCREEN_PREF = "pref_seen_welcome_screen" + private const val PROMPTED_PUSH_REGISTRATION_PREF = "pref_prompted_push_registration" + private const val PROMPTED_DEFAULT_SMS_PREF = "pref_prompted_default_sms" + private const val PROMPTED_OPTIMIZE_DOZE_PREF = "pref_prompted_optimize_doze" + private const val PROMPTED_SHARE_PREF = "pref_prompted_share" + private const val SIGNALING_KEY_PREF = "pref_signaling_key" + private const val DIRECTORY_FRESH_TIME_PREF = "pref_directory_refresh_time" + private const val UPDATE_APK_REFRESH_TIME_PREF = "pref_update_apk_refresh_time" + private const val UPDATE_APK_DOWNLOAD_ID = "pref_update_apk_download_id" + private const val UPDATE_APK_DIGEST = "pref_update_apk_digest" + private const val SIGNED_PREKEY_ROTATION_TIME_PREF = "pref_signed_pre_key_rotation_time" + + private const val IN_THREAD_NOTIFICATION_PREF = "pref_key_inthread_notifications" + private const val SHOW_INVITE_REMINDER_PREF = "pref_show_invite_reminder" + const val MESSAGE_BODY_TEXT_SIZE_PREF = "pref_message_body_text_size" + + private const val LOCAL_REGISTRATION_ID_PREF = "pref_local_registration_id" + private const val SIGNED_PREKEY_REGISTERED_PREF = "pref_signed_prekey_registered" + private const val WIFI_SMS_PREF = "pref_wifi_sms" + + private const val GCM_DISABLED_PREF = "pref_gcm_disabled" + private const val GCM_REGISTRATION_ID_PREF = "pref_gcm_registration_id" + private const val GCM_REGISTRATION_ID_VERSION_PREF = "pref_gcm_registration_id_version" + private const val GCM_REGISTRATION_ID_TIME_PREF = "pref_gcm_registration_id_last_set_time" + private const val WEBSOCKET_REGISTERED_PREF = "pref_websocket_registered" + private const val RATING_LATER_PREF = "pref_rating_later" + private const val RATING_ENABLED_PREF = "pref_rating_enabled" + private const val SIGNED_PREKEY_FAILURE_COUNT_PREF = "pref_signed_prekey_failure_count" + + const val REPEAT_ALERTS_PREF = "pref_repeat_alerts" + const val NOTIFICATION_PRIVACY_PREF = "pref_notification_privacy" + const val NOTIFICATION_PRIORITY_PREF = "pref_notification_priority" + const val NEW_CONTACTS_NOTIFICATIONS = "pref_enable_new_contacts_notifications" + const val WEBRTC_CALLING_PREF = "pref_webrtc_calling" + + const val MEDIA_DOWNLOAD_MOBILE_PREF = "pref_media_download_mobile" + const val MEDIA_DOWNLOAD_WIFI_PREF = "pref_media_download_wifi" + const val MEDIA_DOWNLOAD_ROAMING_PREF = "pref_media_download_roaming" + + const val SYSTEM_EMOJI_PREF = "pref_system_emoji" + private const val MULTI_DEVICE_PROVISIONED_PREF = "pref_multi_device" + const val DIRECT_CAPTURE_CAMERA_ID = "pref_direct_capture_camera_id" + private const val ALWAYS_RELAY_CALLS_PREF = "pref_turn_only" + private const val PROFILE_KEY_PREF = "pref_profile_key" + private const val PROFILE_NAME_PREF = "pref_profile_name" + private const val PROFILE_AVATAR_ID_PREF = "pref_profile_avatar_id" + private const val PROFILE_AVATAR_URL_PREF = "pref_profile_avatar_url" + const val READ_RECEIPTS_PREF = "pref_read_receipts" + const val INCOGNITO_KEYBORAD_PREF = "pref_incognito_keyboard" + private const val UNAUTHORIZED_RECEIVED = "pref_unauthorized_received" + private const val SUCCESSFUL_DIRECTORY_PREF = "pref_successful_directory" + + private const val DATABASE_ENCRYPTED_SECRET = "pref_database_encrypted_secret" + private const val DATABASE_UNENCRYPTED_SECRET = "pref_database_unencrypted_secret" + private const val ATTACHMENT_ENCRYPTED_SECRET = "pref_attachment_encrypted_secret" + private const val ATTACHMENT_UNENCRYPTED_SECRET = "pref_attachment_unencrypted_secret" + private const val NEEDS_SQLCIPHER_MIGRATION = "pref_needs_sql_cipher_migration" + + private const val NEXT_PRE_KEY_ID = "pref_next_pre_key_id" + private const val ACTIVE_SIGNED_PRE_KEY_ID = "pref_active_signed_pre_key_id" + private const val NEXT_SIGNED_PRE_KEY_ID = "pref_next_signed_pre_key_id" + + const val BACKUP_ENABLED = "pref_backup_enabled_v3" + private const val BACKUP_PASSPHRASE = "pref_backup_passphrase" + private const val ENCRYPTED_BACKUP_PASSPHRASE = "pref_encrypted_backup_passphrase" + private const val BACKUP_TIME = "pref_backup_next_time" + const val BACKUP_NOW = "pref_backup_create" + private const val BACKUP_SAVE_DIR = "pref_save_dir" + + const val SCREEN_LOCK = "pref_android_screen_lock" + const val SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout" + + private const val LAST_FULL_CONTACT_SYNC_TIME = "pref_last_full_contact_sync_time" + private const val NEEDS_FULL_CONTACT_SYNC = "pref_needs_full_contact_sync" + + private const val LOG_ENCRYPTED_SECRET = "pref_log_encrypted_secret" + private const val LOG_UNENCRYPTED_SECRET = "pref_log_unencrypted_secret" + + private const val NOTIFICATION_CHANNEL_VERSION = "pref_notification_channel_version" + private const val NOTIFICATION_MESSAGES_CHANNEL_VERSION = "pref_notification_messages_channel_version" + + private const val NEEDS_MESSAGE_PULL = "pref_needs_message_pull" + + private const val UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF = "pref_unidentified_access_certificate_rotation_time" + private const val UNIDENTIFIED_ACCESS_CERTIFICATE = "pref_unidentified_access_certificate" + const val UNIVERSAL_UNIDENTIFIED_ACCESS = "pref_universal_unidentified_access" + const val SHOW_UNIDENTIFIED_DELIVERY_INDICATORS = "pref_show_unidentifed_delivery_indicators" + private const val UNIDENTIFIED_DELIVERY_ENABLED = "pref_unidentified_delivery_enabled" + + const val TYPING_INDICATORS = "pref_typing_indicators" + + const val LINK_PREVIEWS = "pref_link_previews" + + private const val GIF_GRID_LAYOUT = "pref_gif_grid_layout" + + private const val SEEN_STICKER_INTRO_TOOLTIP = "pref_seen_sticker_intro_tooltip" + + private const val MEDIA_KEYBOARD_MODE = "pref_media_keyboard_mode" + + // region FCM + private const val IS_USING_FCM = "pref_is_using_fcm" + private const val FCM_TOKEN = "pref_fcm_token" + private const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2" + private const val HAS_SEEN_PN_MODE_SHEET = "pref_has_seen_pn_mode_sheet" + + fun isUsingFCM(context: Context): Boolean { + return getBooleanPreference(context, IS_USING_FCM, false) + } + + fun setIsUsingFCM(context: Context, value: Boolean) { + setBooleanPreference(context, IS_USING_FCM, value) + } + + fun getFCMToken(context: Context): String? { + return getStringPreference(context, FCM_TOKEN, "") + } + + fun setFCMToken(context: Context, value: String) { + setStringPreference(context, FCM_TOKEN, value) + } + + fun getLastFCMUploadTime(context: Context): Long { + return getLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, 0) + } + + fun setLastFCMUploadTime(context: Context, value: Long) { + setLongPreference(context, LAST_FCM_TOKEN_UPLOAD_TIME, value) + } + + // endregion + fun isScreenLockEnabled(context: Context): Boolean { + return getBooleanPreference(context, SCREEN_LOCK, false) + } + + fun setScreenLockEnabled(context: Context, value: Boolean) { + setBooleanPreference(context, SCREEN_LOCK, value) + } + + fun getScreenLockTimeout(context: Context): Long { + return getLongPreference(context, SCREEN_LOCK_TIMEOUT, 0) + } + + fun setScreenLockTimeout(context: Context, value: Long) { + setLongPreference(context, SCREEN_LOCK_TIMEOUT, value) + } + + fun setBackupPassphrase(context: Context, passphrase: String?) { + setStringPreference(context, BACKUP_PASSPHRASE, passphrase) + } + + fun getBackupPassphrase(context: Context): String? { + return getStringPreference(context, BACKUP_PASSPHRASE, null) + } + + fun setEncryptedBackupPassphrase(context: Context, encryptedPassphrase: String?) { + setStringPreference(context, ENCRYPTED_BACKUP_PASSPHRASE, encryptedPassphrase) + } + + fun getEncryptedBackupPassphrase(context: Context): String? { + return getStringPreference(context, ENCRYPTED_BACKUP_PASSPHRASE, null) + } + + fun setBackupEnabled(context: Context, value: Boolean) { + setBooleanPreference(context, BACKUP_ENABLED, value) + } + + fun isBackupEnabled(context: Context): Boolean { + return getBooleanPreference(context, BACKUP_ENABLED, false) + } + + fun setNextBackupTime(context: Context, time: Long) { + setLongPreference(context, BACKUP_TIME, time) + } + + fun getNextBackupTime(context: Context): Long { + return getLongPreference(context, BACKUP_TIME, -1) + } + + fun setBackupSaveDir(context: Context, dirUri: String?) { + setStringPreference(context, BACKUP_SAVE_DIR, dirUri) + } + + fun getBackupSaveDir(context: Context): String? { + return getStringPreference(context, BACKUP_SAVE_DIR, null) + } + + fun getNextPreKeyId(context: Context): Int { + return getIntegerPreference(context, NEXT_PRE_KEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + } + + fun setNextPreKeyId(context: Context, value: Int) { + setIntegerPrefrence(context, NEXT_PRE_KEY_ID, value) + } + + fun getNextSignedPreKeyId(context: Context): Int { + return getIntegerPreference(context, NEXT_SIGNED_PRE_KEY_ID, SecureRandom().nextInt(Medium.MAX_VALUE)) + } + + fun setNextSignedPreKeyId(context: Context, value: Int) { + setIntegerPrefrence(context, NEXT_SIGNED_PRE_KEY_ID, value) + } + + fun getActiveSignedPreKeyId(context: Context): Int { + return getIntegerPreference(context, ACTIVE_SIGNED_PRE_KEY_ID, -1) + } + + fun setActiveSignedPreKeyId(context: Context, value: Int) { + setIntegerPrefrence(context, ACTIVE_SIGNED_PRE_KEY_ID, value) + } + + // TODO +// fun setNeedsSqlCipherMigration(context: Context, value: Boolean) { +// setBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, value) +// org.greenrobot.eventbus.EventBus.getDefault().post(SqlCipherNeedsMigrationEvent()) +// } + + fun getNeedsSqlCipherMigration(context: Context): Boolean { + return getBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, false) + } + + fun setAttachmentEncryptedSecret(context: Context, secret: String) { + setStringPreference(context, ATTACHMENT_ENCRYPTED_SECRET, secret) + } + + fun setAttachmentUnencryptedSecret(context: Context, secret: String?) { + setStringPreference(context, ATTACHMENT_UNENCRYPTED_SECRET, secret) + } + + fun getAttachmentEncryptedSecret(context: Context): String? { + return getStringPreference(context, ATTACHMENT_ENCRYPTED_SECRET, null) + } + + fun getAttachmentUnencryptedSecret(context: Context): String? { + return getStringPreference(context, ATTACHMENT_UNENCRYPTED_SECRET, null) + } + + fun setDatabaseEncryptedSecret(context: Context, secret: String) { + setStringPreference(context, DATABASE_ENCRYPTED_SECRET, secret) + } + + fun setDatabaseUnencryptedSecret(context: Context, secret: String?) { + setStringPreference(context, DATABASE_UNENCRYPTED_SECRET, secret) + } + + fun getDatabaseUnencryptedSecret(context: Context): String? { + return getStringPreference(context, DATABASE_UNENCRYPTED_SECRET, null) + } + + fun getDatabaseEncryptedSecret(context: Context): String? { + return getStringPreference(context, DATABASE_ENCRYPTED_SECRET, null) + } + + fun setHasSuccessfullyRetrievedDirectory(context: Context, value: Boolean) { + setBooleanPreference(context, SUCCESSFUL_DIRECTORY_PREF, value) + } + + fun hasSuccessfullyRetrievedDirectory(context: Context): Boolean { + return getBooleanPreference(context, SUCCESSFUL_DIRECTORY_PREF, false) + } + + fun setUnauthorizedReceived(context: Context, value: Boolean) { + setBooleanPreference(context, UNAUTHORIZED_RECEIVED, value) + } + + fun isUnauthorizedRecieved(context: Context): Boolean { + return getBooleanPreference(context, UNAUTHORIZED_RECEIVED, false) + } + + fun isIncognitoKeyboardEnabled(context: Context): Boolean { + return getBooleanPreference(context, INCOGNITO_KEYBORAD_PREF, true) + } + + fun isReadReceiptsEnabled(context: Context): Boolean { + return getBooleanPreference(context, READ_RECEIPTS_PREF, false) + } + + fun setReadReceiptsEnabled(context: Context, enabled: Boolean) { + setBooleanPreference(context, READ_RECEIPTS_PREF, enabled) + } + + fun isTypingIndicatorsEnabled(context: Context): Boolean { + return getBooleanPreference(context, TYPING_INDICATORS, false) + } + + fun setTypingIndicatorsEnabled(context: Context, enabled: Boolean) { + setBooleanPreference(context, TYPING_INDICATORS, enabled) + } + + fun isLinkPreviewsEnabled(context: Context): Boolean { + return getBooleanPreference(context, LINK_PREVIEWS, false) + } + + fun setLinkPreviewsEnabled(context: Context, enabled: Boolean) { + setBooleanPreference(context, LINK_PREVIEWS, enabled) + } + + fun isGifSearchInGridLayout(context: Context): Boolean { + return getBooleanPreference(context, GIF_GRID_LAYOUT, false) + } + + fun setIsGifSearchInGridLayout(context: Context, isGrid: Boolean) { + setBooleanPreference(context, GIF_GRID_LAYOUT, isGrid) + } + + fun getProfileKey(context: Context): String? { + return getStringPreference(context, PROFILE_KEY_PREF, null) + } + + fun setProfileKey(context: Context, key: String?) { + setStringPreference(context, PROFILE_KEY_PREF, key) + } + + fun setProfileName(context: Context, name: String?) { + setStringPreference(context, PROFILE_NAME_PREF, name) + } + + fun getProfileName(context: Context): String? { + return getStringPreference(context, PROFILE_NAME_PREF, null) + } + + fun setProfileAvatarId(context: Context, id: Int) { + setIntegerPrefrence(context, PROFILE_AVATAR_ID_PREF, id) + } + + fun getProfileAvatarId(context: Context): Int { + return getIntegerPreference(context, PROFILE_AVATAR_ID_PREF, 0) + } + + fun setProfilePictureURL(context: Context, url: String?) { + setStringPreference(context, PROFILE_AVATAR_URL_PREF, url) + } + + fun getProfilePictureURL(context: Context): String? { + return getStringPreference(context, PROFILE_AVATAR_URL_PREF, null) + } + + fun getNotificationPriority(context: Context): Int { + return getStringPreference(context, NOTIFICATION_PRIORITY_PREF, NotificationCompat.PRIORITY_HIGH.toString())!!.toInt() + } + + fun getMessageBodyTextSize(context: Context): Int { + return getStringPreference(context, MESSAGE_BODY_TEXT_SIZE_PREF, "16")!!.toInt() + } + + fun isTurnOnly(context: Context): Boolean { + return getBooleanPreference(context, ALWAYS_RELAY_CALLS_PREF, false) + } + + fun isFcmDisabled(context: Context): Boolean { + return getBooleanPreference(context, GCM_DISABLED_PREF, false) + } + + fun setFcmDisabled(context: Context, disabled: Boolean) { + setBooleanPreference(context, GCM_DISABLED_PREF, disabled) + } + + fun isWebrtcCallingEnabled(context: Context): Boolean { + return getBooleanPreference(context, WEBRTC_CALLING_PREF, false) + } + + fun setWebrtcCallingEnabled(context: Context, enabled: Boolean) { + setBooleanPreference(context, WEBRTC_CALLING_PREF, enabled) + } + + fun setDirectCaptureCameraId(context: Context, value: Int) { + setIntegerPrefrence(context, DIRECT_CAPTURE_CAMERA_ID, value) + } + + fun getDirectCaptureCameraId(context: Context): Int { + return getIntegerPreference(context, DIRECT_CAPTURE_CAMERA_ID, Camera.CameraInfo.CAMERA_FACING_FRONT) + } + + fun setMultiDevice(context: Context, value: Boolean) { + setBooleanPreference(context, MULTI_DEVICE_PROVISIONED_PREF, value) + } + + fun isMultiDevice(context: Context): Boolean { + return getBooleanPreference(context, MULTI_DEVICE_PROVISIONED_PREF, false) + } + + fun setSignedPreKeyFailureCount(context: Context, value: Int) { + setIntegerPrefrence(context, SIGNED_PREKEY_FAILURE_COUNT_PREF, value) + } + + fun getSignedPreKeyFailureCount(context: Context): Int { + return getIntegerPreference(context, SIGNED_PREKEY_FAILURE_COUNT_PREF, 0) + } + + // TODO +// fun getNotificationPrivacy(context: Context): NotificationPrivacyPreference { +// return NotificationPrivacyPreference(getStringPreference(context, NOTIFICATION_PRIVACY_PREF, "all")) +// } + + fun isNewContactsNotificationEnabled(context: Context): Boolean { + return getBooleanPreference(context, NEW_CONTACTS_NOTIFICATIONS, true) + } + + fun getRatingLaterTimestamp(context: Context): Long { + return getLongPreference(context, RATING_LATER_PREF, 0) + } + + fun setRatingLaterTimestamp(context: Context, timestamp: Long) { + setLongPreference(context, RATING_LATER_PREF, timestamp) + } + + fun isRatingEnabled(context: Context): Boolean { + return getBooleanPreference(context, RATING_ENABLED_PREF, true) + } + + fun setRatingEnabled(context: Context, enabled: Boolean) { + setBooleanPreference(context, RATING_ENABLED_PREF, enabled) + } + + fun isWebsocketRegistered(context: Context): Boolean { + return getBooleanPreference(context, WEBSOCKET_REGISTERED_PREF, false) + } + + fun setWebsocketRegistered(context: Context, registered: Boolean) { + setBooleanPreference(context, WEBSOCKET_REGISTERED_PREF, registered) + } + + fun isWifiSmsEnabled(context: Context): Boolean { + return getBooleanPreference(context, WIFI_SMS_PREF, false) + } + + fun getRepeatAlertsCount(context: Context): Int { + return try { + getStringPreference(context, REPEAT_ALERTS_PREF, "0")!!.toInt() + } catch (e: NumberFormatException) { + Log.w(TAG, e) + 0 + } + } + + fun setRepeatAlertsCount(context: Context, count: Int) { + setStringPreference(context, REPEAT_ALERTS_PREF, count.toString()) + } + + fun isSignedPreKeyRegistered(context: Context): Boolean { + return getBooleanPreference(context, SIGNED_PREKEY_REGISTERED_PREF, false) + } + + fun setSignedPreKeyRegistered(context: Context, value: Boolean) { + setBooleanPreference(context, SIGNED_PREKEY_REGISTERED_PREF, value) + } + + fun getLocalRegistrationId(context: Context): Int { + return getIntegerPreference(context, LOCAL_REGISTRATION_ID_PREF, 0) + } + + fun setLocalRegistrationId(context: Context, registrationId: Int) { + setIntegerPrefrence(context, LOCAL_REGISTRATION_ID_PREF, registrationId) + } + + fun removeLocalRegistrationId(context: Context) { + removePreference(context, LOCAL_REGISTRATION_ID_PREF) + } + + fun isInThreadNotifications(context: Context): Boolean { + return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true) + } + + fun getUnidentifiedAccessCertificateRotationTime(context: Context): Long { + return getLongPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF, 0L) + } + + fun setUnidentifiedAccessCertificateRotationTime(context: Context, value: Long) { + setLongPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF, value) + } + + fun setUnidentifiedAccessCertificate(context: Context, value: ByteArray?) { + setStringPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE, Base64.encodeBytes(value)) + } + + fun getUnidentifiedAccessCertificate(context: Context): ByteArray? { + try { + val result = getStringPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE, null) + if (result != null) { + return Base64.decode(result) + } + } catch (e: IOException) { + Log.w(TAG, e) + } + return null + } + + fun isUniversalUnidentifiedAccess(context: Context): Boolean { + return getBooleanPreference(context, UNIVERSAL_UNIDENTIFIED_ACCESS, false) + } + + fun isShowUnidentifiedDeliveryIndicatorsEnabled(context: Context): Boolean { + return getBooleanPreference(context, SHOW_UNIDENTIFIED_DELIVERY_INDICATORS, false) + } + + fun setIsUnidentifiedDeliveryEnabled(context: Context, enabled: Boolean) { + setBooleanPreference(context, UNIDENTIFIED_DELIVERY_ENABLED, enabled) + } + + fun isUnidentifiedDeliveryEnabled(context: Context): Boolean { + // Loki - Always enable unidentified sender + return true + // return getBooleanPreference(context, UNIDENTIFIED_DELIVERY_ENABLED, true); + } + + fun getSignedPreKeyRotationTime(context: Context): Long { + return getLongPreference(context, SIGNED_PREKEY_ROTATION_TIME_PREF, 0L) + } + + fun setSignedPreKeyRotationTime(context: Context, value: Long) { + setLongPreference(context, SIGNED_PREKEY_ROTATION_TIME_PREF, value) + } + + fun getDirectoryRefreshTime(context: Context): Long { + return getLongPreference(context, DIRECTORY_FRESH_TIME_PREF, 0L) + } + + fun setDirectoryRefreshTime(context: Context, value: Long) { + setLongPreference(context, DIRECTORY_FRESH_TIME_PREF, value) + } + + fun getUpdateApkRefreshTime(context: Context): Long { + return getLongPreference(context, UPDATE_APK_REFRESH_TIME_PREF, 0L) + } + + fun setUpdateApkRefreshTime(context: Context, value: Long) { + setLongPreference(context, UPDATE_APK_REFRESH_TIME_PREF, value) + } + + fun setUpdateApkDownloadId(context: Context, value: Long) { + setLongPreference(context, UPDATE_APK_DOWNLOAD_ID, value) + } + + fun getUpdateApkDownloadId(context: Context): Long { + return getLongPreference(context, UPDATE_APK_DOWNLOAD_ID, -1) + } + + fun setUpdateApkDigest(context: Context, value: String?) { + setStringPreference(context, UPDATE_APK_DIGEST, value) + } + + fun getUpdateApkDigest(context: Context): String? { + return getStringPreference(context, UPDATE_APK_DIGEST, null) + } + + fun getLocalNumber(context: Context): String? { + return getStringPreference(context, LOCAL_NUMBER_PREF, null) + } + + fun setLocalNumber(context: Context, localNumber: String) { + setStringPreference(context, LOCAL_NUMBER_PREF, localNumber.toLowerCase()) + } + + fun removeLocalNumber(context: Context) { + removePreference(context, LOCAL_NUMBER_PREF) + } + + fun getPushServerPassword(context: Context): String? { + return getStringPreference(context, GCM_PASSWORD_PREF, null) + } + + fun setPushServerPassword(context: Context, password: String?) { + setStringPreference(context, GCM_PASSWORD_PREF, password) + } + + fun getSignalingKey(context: Context): String? { + return getStringPreference(context, SIGNALING_KEY_PREF, null) + } + + fun isEnterImeKeyEnabled(context: Context): Boolean { + return getBooleanPreference(context, ENTER_PRESENT_PREF, false) + } + + fun isEnterSendsEnabled(context: Context): Boolean { + return getBooleanPreference(context, ENTER_SENDS_PREF, false) + } + + fun isPasswordDisabled(context: Context): Boolean { + return getBooleanPreference(context, DISABLE_PASSPHRASE_PREF, false) + } + + fun setPasswordDisabled(context: Context, disabled: Boolean) { + setBooleanPreference(context, DISABLE_PASSPHRASE_PREF, disabled) + } + + fun getUseCustomMmsc(context: Context): Boolean { + val legacy: Boolean = isLegacyUseLocalApnsEnabled(context) + return getBooleanPreference(context, MMSC_CUSTOM_HOST_PREF, legacy) + } + + fun setUseCustomMmsc(context: Context, value: Boolean) { + setBooleanPreference(context, MMSC_CUSTOM_HOST_PREF, value) + } + + fun getMmscUrl(context: Context): String? { + return getStringPreference(context, MMSC_HOST_PREF, "") + } + + fun setMmscUrl(context: Context, mmsc: String?) { + setStringPreference(context, MMSC_HOST_PREF, mmsc) + } + + fun getUseCustomMmscProxy(context: Context): Boolean { + val legacy: Boolean = isLegacyUseLocalApnsEnabled(context) + return getBooleanPreference(context, MMSC_CUSTOM_PROXY_PREF, legacy) + } + + fun setUseCustomMmscProxy(context: Context, value: Boolean) { + setBooleanPreference(context, MMSC_CUSTOM_PROXY_PREF, value) + } + + fun getMmscProxy(context: Context): String? { + return getStringPreference(context, MMSC_PROXY_HOST_PREF, "") + } + + fun setMmscProxy(context: Context, value: String?) { + setStringPreference(context, MMSC_PROXY_HOST_PREF, value) + } + + fun getUseCustomMmscProxyPort(context: Context): Boolean { + val legacy: Boolean = isLegacyUseLocalApnsEnabled(context) + return getBooleanPreference(context, MMSC_CUSTOM_PROXY_PORT_PREF, legacy) + } + + fun setUseCustomMmscProxyPort(context: Context, value: Boolean) { + setBooleanPreference(context, MMSC_CUSTOM_PROXY_PORT_PREF, value) + } + + fun getMmscProxyPort(context: Context): String? { + return getStringPreference(context, MMSC_PROXY_PORT_PREF, "") + } + + fun setMmscProxyPort(context: Context, value: String?) { + setStringPreference(context, MMSC_PROXY_PORT_PREF, value) + } + + fun getUseCustomMmscUsername(context: Context): Boolean { + val legacy: Boolean = isLegacyUseLocalApnsEnabled(context) + return getBooleanPreference(context, MMSC_CUSTOM_USERNAME_PREF, legacy) + } + + fun setUseCustomMmscUsername(context: Context, value: Boolean) { + setBooleanPreference(context, MMSC_CUSTOM_USERNAME_PREF, value) + } + + fun getMmscUsername(context: Context): String? { + return getStringPreference(context, MMSC_USERNAME_PREF, "") + } + + fun setMmscUsername(context: Context, value: String?) { + setStringPreference(context, MMSC_USERNAME_PREF, value) + } + + fun getUseCustomMmscPassword(context: Context): Boolean { + val legacy: Boolean = isLegacyUseLocalApnsEnabled(context) + return getBooleanPreference(context, MMSC_CUSTOM_PASSWORD_PREF, legacy) + } + + fun setUseCustomMmscPassword(context: Context, value: Boolean) { + setBooleanPreference(context, MMSC_CUSTOM_PASSWORD_PREF, value) + } + + fun getMmscPassword(context: Context): String? { + return getStringPreference(context, MMSC_PASSWORD_PREF, "") + } + + fun setMmscPassword(context: Context, value: String?) { + setStringPreference(context, MMSC_PASSWORD_PREF, value) + } + + fun getMmsUserAgent(context: Context, defaultUserAgent: String): String { + val useCustom: Boolean = getBooleanPreference(context, MMS_CUSTOM_USER_AGENT, false) + return if (useCustom) getStringPreference(context, MMS_USER_AGENT, defaultUserAgent)!! else defaultUserAgent + } + + fun getIdentityContactUri(context: Context): String? { + return getStringPreference(context, IDENTITY_PREF, null) + } + + fun setIdentityContactUri(context: Context, identityUri: String?) { + setStringPreference(context, IDENTITY_PREF, identityUri) + } + + fun setScreenSecurityEnabled(context: Context, value: Boolean) { + setBooleanPreference(context, SCREEN_SECURITY_PREF, value) + } + + fun isScreenSecurityEnabled(context: Context): Boolean { + return getBooleanPreference(context, SCREEN_SECURITY_PREF, true) + } + + fun isLegacyUseLocalApnsEnabled(context: Context): Boolean { + return getBooleanPreference(context, ENABLE_MANUAL_MMS_PREF, false) + } + + fun getLastVersionCode(context: Context): Int { + return getIntegerPreference(context, LAST_VERSION_CODE_PREF, 0) + } + + @Throws(IOException::class) + fun setLastVersionCode(context: Context, versionCode: Int) { + if (!setIntegerPrefrenceBlocking(context, LAST_VERSION_CODE_PREF, versionCode)) { + throw IOException("couldn't write version code to sharedpreferences") + } + } + + fun getLastExperienceVersionCode(context: Context): Int { + return getIntegerPreference(context, LAST_EXPERIENCE_VERSION_PREF, 0) + } + + fun setLastExperienceVersionCode(context: Context, versionCode: Int) { + setIntegerPrefrence(context, LAST_EXPERIENCE_VERSION_PREF, versionCode) + } + + fun getExperienceDismissedVersionCode(context: Context): Int { + return getIntegerPreference(context, EXPERIENCE_DISMISSED_PREF, 0) + } + + fun setExperienceDismissedVersionCode(context: Context, versionCode: Int) { + setIntegerPrefrence(context, EXPERIENCE_DISMISSED_PREF, versionCode) + } + + fun getTheme(context: Context): String? { + return getStringPreference(context, THEME_PREF, "light") + } + + fun isVerifying(context: Context): Boolean { + return getBooleanPreference(context, VERIFYING_STATE_PREF, false) + } + + fun setVerifying(context: Context, verifying: Boolean) { + setBooleanPreference(context, VERIFYING_STATE_PREF, verifying) + } + + fun isPushRegistered(context: Context): Boolean { + return getBooleanPreference(context, REGISTERED_GCM_PREF, false) + } + + fun setPushRegistered(context: Context, registered: Boolean) { + Log.i(TAG, "Setting push registered: $registered") + setBooleanPreference(context, REGISTERED_GCM_PREF, registered) + } + + fun isShowInviteReminders(context: Context): Boolean { + return getBooleanPreference(context, SHOW_INVITE_REMINDER_PREF, true) + } + + fun isPassphraseTimeoutEnabled(context: Context): Boolean { + return getBooleanPreference(context, PASSPHRASE_TIMEOUT_PREF, false) + } + + fun getPassphraseTimeoutInterval(context: Context): Int { + return getIntegerPreference(context, PASSPHRASE_TIMEOUT_INTERVAL_PREF, 5 * 60) + } + + fun setPassphraseTimeoutInterval(context: Context, interval: Int) { + setIntegerPrefrence(context, PASSPHRASE_TIMEOUT_INTERVAL_PREF, interval) + } + + fun getLanguage(context: Context): String? { + return getStringPreference(context, LANGUAGE_PREF, "zz") + } + + fun setLanguage(context: Context, language: String?) { + setStringPreference(context, LANGUAGE_PREF, language) + } + + fun isSmsDeliveryReportsEnabled(context: Context): Boolean { + return getBooleanPreference(context, SMS_DELIVERY_REPORT_PREF, false) + } + + fun hasSeenWelcomeScreen(context: Context): Boolean { + return getBooleanPreference(context, SEEN_WELCOME_SCREEN_PREF, true) + } + + fun setHasSeenWelcomeScreen(context: Context, value: Boolean) { + setBooleanPreference(context, SEEN_WELCOME_SCREEN_PREF, value) + } + + fun hasPromptedPushRegistration(context: Context): Boolean { + return getBooleanPreference(context, PROMPTED_PUSH_REGISTRATION_PREF, false) + } + + fun setPromptedPushRegistration(context: Context, value: Boolean) { + setBooleanPreference(context, PROMPTED_PUSH_REGISTRATION_PREF, value) + } + + fun hasPromptedDefaultSmsProvider(context: Context): Boolean { + return getBooleanPreference(context, PROMPTED_DEFAULT_SMS_PREF, false) + } + + fun setPromptedDefaultSmsProvider(context: Context, value: Boolean) { + setBooleanPreference(context, PROMPTED_DEFAULT_SMS_PREF, value) + } + + fun setPromptedOptimizeDoze(context: Context, value: Boolean) { + setBooleanPreference(context, PROMPTED_OPTIMIZE_DOZE_PREF, value) + } + + fun hasPromptedOptimizeDoze(context: Context): Boolean { + return getBooleanPreference(context, PROMPTED_OPTIMIZE_DOZE_PREF, false) + } + + fun hasPromptedShare(context: Context): Boolean { + return getBooleanPreference(context, PROMPTED_SHARE_PREF, false) + } + + fun setPromptedShare(context: Context, value: Boolean) { + setBooleanPreference(context, PROMPTED_SHARE_PREF, value) + } + + fun isInterceptAllMmsEnabled(context: Context): Boolean { + return getBooleanPreference(context, ALL_MMS_PREF, true) + } + + fun isInterceptAllSmsEnabled(context: Context): Boolean { + return getBooleanPreference(context, ALL_SMS_PREF, true) + } + + fun isNotificationsEnabled(context: Context): Boolean { + return getBooleanPreference(context, NOTIFICATION_PREF, true) + } + + fun getNotificationRingtone(context: Context): Uri { + var result = getStringPreference(context, RINGTONE_PREF, Settings.System.DEFAULT_NOTIFICATION_URI.toString()) + if (result != null && result.startsWith("file:")) { + result = Settings.System.DEFAULT_NOTIFICATION_URI.toString() + } + return Uri.parse(result) + } + + fun removeNotificationRingtone(context: Context) { + removePreference(context, RINGTONE_PREF) + } + + fun setNotificationRingtone(context: Context, ringtone: String?) { + setStringPreference(context, RINGTONE_PREF, ringtone) + } + + fun setNotificationVibrateEnabled(context: Context, enabled: Boolean) { + setBooleanPreference(context, VIBRATE_PREF, enabled) + } + + fun isNotificationVibrateEnabled(context: Context): Boolean { + return getBooleanPreference(context, VIBRATE_PREF, true) + } + + fun getNotificationLedColor(context: Context): String? { + return getStringPreference(context, LED_COLOR_PREF, "blue") + } + + fun getNotificationLedPattern(context: Context): String? { + return getStringPreference(context, LED_BLINK_PREF, "500,2000") + } + + fun getNotificationLedPatternCustom(context: Context): String? { + return getStringPreference(context, LED_BLINK_PREF_CUSTOM, "500,2000") + } + + fun setNotificationLedPatternCustom(context: Context, pattern: String?) { + setStringPreference(context, LED_BLINK_PREF_CUSTOM, pattern) + } + + fun isThreadLengthTrimmingEnabled(context: Context): Boolean { + return getBooleanPreference(context, THREAD_TRIM_ENABLED, false) + } + + fun getThreadTrimLength(context: Context): Int { + return getStringPreference(context, THREAD_TRIM_LENGTH, "500")!!.toInt() + } + + fun isSystemEmojiPreferred(context: Context): Boolean { + return getBooleanPreference(context, SYSTEM_EMOJI_PREF, false) + } + + // TODO +// fun getMobileMediaDownloadAllowed(context: Context): Set { +// return getMediaDownloadAllowed(context, MEDIA_DOWNLOAD_MOBILE_PREF, R.array.pref_media_download_mobile_data_default) +// } +// +// fun getWifiMediaDownloadAllowed(context: Context): Set { +// return getMediaDownloadAllowed(context, MEDIA_DOWNLOAD_WIFI_PREF, R.array.pref_media_download_wifi_default) +// } +// +// fun getRoamingMediaDownloadAllowed(context: Context): Set { +// return getMediaDownloadAllowed(context, MEDIA_DOWNLOAD_ROAMING_PREF, R.array.pref_media_download_roaming_default) +// } + + private fun getMediaDownloadAllowed(context: Context, key: String, @ArrayRes defaultValuesRes: Int): Set? { + return getStringSetPreference(context, key, HashSet(Arrays.asList(*context.resources.getStringArray(defaultValuesRes)))) + } + + fun getLastFullContactSyncTime(context: Context): Long { + return getLongPreference(context, LAST_FULL_CONTACT_SYNC_TIME, 0) + } + + fun setLastFullContactSyncTime(context: Context, timestamp: Long) { + setLongPreference(context, LAST_FULL_CONTACT_SYNC_TIME, timestamp) + } + + fun needsFullContactSync(context: Context): Boolean { + return getBooleanPreference(context, NEEDS_FULL_CONTACT_SYNC, false) + } + + fun setNeedsFullContactSync(context: Context, needsSync: Boolean) { + setBooleanPreference(context, NEEDS_FULL_CONTACT_SYNC, needsSync) + } + + fun setLogEncryptedSecret(context: Context, base64Secret: String?) { + setStringPreference(context, LOG_ENCRYPTED_SECRET, base64Secret) + } + + fun getLogEncryptedSecret(context: Context): String? { + return getStringPreference(context, LOG_ENCRYPTED_SECRET, null) + } + + fun setLogUnencryptedSecret(context: Context, base64Secret: String?) { + setStringPreference(context, LOG_UNENCRYPTED_SECRET, base64Secret) + } + + fun getLogUnencryptedSecret(context: Context): String? { + return getStringPreference(context, LOG_UNENCRYPTED_SECRET, null) + } + + fun getNotificationChannelVersion(context: Context): Int { + return getIntegerPreference(context, NOTIFICATION_CHANNEL_VERSION, 1) + } + + fun setNotificationChannelVersion(context: Context, version: Int) { + setIntegerPrefrence(context, NOTIFICATION_CHANNEL_VERSION, version) + } + + fun getNotificationMessagesChannelVersion(context: Context): Int { + return getIntegerPreference(context, NOTIFICATION_MESSAGES_CHANNEL_VERSION, 1) + } + + fun setNotificationMessagesChannelVersion(context: Context, version: Int) { + setIntegerPrefrence(context, NOTIFICATION_MESSAGES_CHANNEL_VERSION, version) + } + + fun getNeedsMessagePull(context: Context): Boolean { + return getBooleanPreference(context, NEEDS_MESSAGE_PULL, false) + } + + fun setNeedsMessagePull(context: Context, needsMessagePull: Boolean) { + setBooleanPreference(context, NEEDS_MESSAGE_PULL, needsMessagePull) + } + + fun hasSeenStickerIntroTooltip(context: Context): Boolean { + return getBooleanPreference(context, SEEN_STICKER_INTRO_TOOLTIP, false) + } + + fun setHasSeenStickerIntroTooltip(context: Context, seenStickerTooltip: Boolean) { + setBooleanPreference(context, SEEN_STICKER_INTRO_TOOLTIP, seenStickerTooltip) + } + + fun setMediaKeyboardMode(context: Context, mode: MediaKeyboardMode) { + setStringPreference(context, MEDIA_KEYBOARD_MODE, mode.name) + } + + fun getMediaKeyboardMode(context: Context): MediaKeyboardMode { + val name = getStringPreference(context, MEDIA_KEYBOARD_MODE, MediaKeyboardMode.EMOJI.name)!! + return MediaKeyboardMode.valueOf(name) + } + + fun setBooleanPreference(context: Context, key: String?, value: Boolean) { + getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply() + } + + fun getBooleanPreference(context: Context, key: String?, defaultValue: Boolean): Boolean { + return getDefaultSharedPreferences(context).getBoolean(key, defaultValue) + } + + fun setStringPreference(context: Context, key: String?, value: String?) { + getDefaultSharedPreferences(context).edit().putString(key, value).apply() + } + + fun getStringPreference(context: Context, key: String, defaultValue: String?): String? { + return getDefaultSharedPreferences(context).getString(key, defaultValue) + } + + private fun getIntegerPreference(context: Context, key: String, defaultValue: Int): Int { + return getDefaultSharedPreferences(context).getInt(key, defaultValue) + } + + private fun setIntegerPrefrence(context: Context, key: String, value: Int) { + getDefaultSharedPreferences(context).edit().putInt(key, value).apply() + } + + private fun setIntegerPrefrenceBlocking(context: Context, key: String, value: Int): Boolean { + return getDefaultSharedPreferences(context).edit().putInt(key, value).commit() + } + + private fun getLongPreference(context: Context, key: String, defaultValue: Long): Long { + return getDefaultSharedPreferences(context).getLong(key, defaultValue) + } + + private fun setLongPreference(context: Context, key: String, value: Long) { + getDefaultSharedPreferences(context).edit().putLong(key, value).apply() + } + + private fun removePreference(context: Context, key: String) { + getDefaultSharedPreferences(context).edit().remove(key).apply() + } + + private fun getStringSetPreference(context: Context, key: String, defaultValues: Set): Set? { + val prefs = getDefaultSharedPreferences(context) + return if (prefs.contains(key)) { + prefs.getStringSet(key, emptySet()) + } else { + defaultValues + } + } + + // region Loki + fun getBackgroundPollTime(context: Context): Long { + return getLongPreference(context, "background_poll_time", 0L) + } + + fun setBackgroundPollTime(context: Context, backgroundPollTime: Long) { + setLongPreference(context, "background_poll_time", backgroundPollTime) + } + + fun getOpenGroupBackgroundPollTime(context: Context): Long { + return getLongPreference(context, "public_chat_background_poll_time", 0L) + } + + fun setOpenGroupBackgroundPollTime(context: Context, backgroundPollTime: Long) { + setLongPreference(context, "public_chat_background_poll_time", backgroundPollTime) + } + + fun isChatSetUp(context: Context, id: String): Boolean { + return getBooleanPreference(context, "is_chat_set_up?chat=$id", false) + } + + fun markChatSetUp(context: Context, id: String) { + setBooleanPreference(context, "is_chat_set_up?chat=$id", true) + } + + fun getMasterHexEncodedPublicKey(context: Context): String? { + return getStringPreference(context, "master_hex_encoded_public_key", null) + } + + fun setMasterHexEncodedPublicKey(context: Context, masterHexEncodedPublicKey: String) { + setStringPreference(context, "master_hex_encoded_public_key", masterHexEncodedPublicKey.toLowerCase()) + } + + fun getHasViewedSeed(context: Context): Boolean { + return getBooleanPreference(context, "has_viewed_seed", false) + } + + fun setHasViewedSeed(context: Context, hasViewedSeed: Boolean) { + setBooleanPreference(context, "has_viewed_seed", hasViewedSeed) + } + + fun setNeedsDatabaseReset(context: Context, resetDatabase: Boolean) { + getDefaultSharedPreferences(context).edit().putBoolean("database_reset", resetDatabase).commit() + } + + fun getNeedsDatabaseReset(context: Context): Boolean { + return getBooleanPreference(context, "database_reset", false) + } + + fun setWasUnlinked(context: Context, value: Boolean) { + // We do it this way so that it gets persisted in storage straight away + getDefaultSharedPreferences(context).edit().putBoolean("database_reset_unpair", value).commit() + } + + fun getWasUnlinked(context: Context): Boolean { + return getBooleanPreference(context, "database_reset_unpair", false) + } + + fun setNeedsIsRevokedSlaveDeviceCheck(context: Context, value: Boolean) { + setBooleanPreference(context, "needs_revocation", value) + } + + fun getNeedsIsRevokedSlaveDeviceCheck(context: Context): Boolean { + return getBooleanPreference(context, "needs_revocation", false) + } + + fun setRestorationTime(context: Context, time: Long) { + setLongPreference(context, "restoration_time", time) + } + + fun getRestorationTime(context: Context): Long { + return getLongPreference(context, "restoration_time", 0) + } + + fun getHasSeenOpenGroupSuggestionSheet(context: Context): Boolean { + return getBooleanPreference(context, "has_seen_open_group_suggestion_sheet", false) + } + + fun setHasSeenOpenGroupSuggestionSheet(context: Context) { + setBooleanPreference(context, "has_seen_open_group_suggestion_sheet", true) + } + + fun getLastProfilePictureUpload(context: Context): Long { + return getLongPreference(context, "last_profile_picture_upload", 0) + } + + fun setLastProfilePictureUpload(context: Context, newValue: Long) { + setLongPreference(context, "last_profile_picture_upload", newValue) + } + + fun hasSeenGIFMetaDataWarning(context: Context): Boolean { + return getBooleanPreference(context, "has_seen_gif_metadata_warning", false) + } + + fun setHasSeenGIFMetaDataWarning(context: Context) { + setBooleanPreference(context, "has_seen_gif_metadata_warning", true) + } + + fun clearAll(context: Context) { + getDefaultSharedPreferences(context).edit().clear().commit() + } + + fun getHasSeenMultiDeviceRemovalSheet(context: Context): Boolean { + return getBooleanPreference(context, "has_seen_multi_device_removal_sheet", false) + } + + fun setHasSeenMultiDeviceRemovalSheet(context: Context) { + setBooleanPreference(context, "has_seen_multi_device_removal_sheet", true) + } + + fun hasSeenLightThemeIntroSheet(context: Context): Boolean { + return getBooleanPreference(context, "has_seen_light_theme_intro_sheet", false) + } + + fun setHasSeenLightThemeIntroSheet(context: Context) { + setBooleanPreference(context, "has_seen_light_theme_intro_sheet", true) + } + + // endregion + /* TODO + // region Backup related + fun getBackupRecords(context: Context): List { + val preferences = getDefaultSharedPreferences(context) + val prefsFileName: String + prefsFileName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + getDefaultSharedPreferencesName(context) + } else { + context.packageName + "_preferences" + } + val prefList: LinkedList = LinkedList() + addBackupEntryInt(prefList, preferences, prefsFileName, LOCAL_REGISTRATION_ID_PREF) + addBackupEntryString(prefList, preferences, prefsFileName, LOCAL_NUMBER_PREF) + addBackupEntryString(prefList, preferences, prefsFileName, PROFILE_NAME_PREF) + addBackupEntryString(prefList, preferences, prefsFileName, PROFILE_AVATAR_URL_PREF) + addBackupEntryInt(prefList, preferences, prefsFileName, PROFILE_AVATAR_ID_PREF) + addBackupEntryString(prefList, preferences, prefsFileName, PROFILE_KEY_PREF) + addBackupEntryBoolean(prefList, preferences, prefsFileName, IS_USING_FCM) + return prefList + } + + private fun addBackupEntryString( + outPrefList: MutableList, + prefs: SharedPreferences, + prefFileName: String, + prefKey: String, + ) { + val value = prefs.getString(prefKey, null) + if (value == null) { + logBackupEntry(prefKey, false) + return + } + outPrefList.add(BackupProtos.SharedPreference.newBuilder() + .setFile(prefFileName) + .setKey(prefKey) + .setValue(value) + .build()) + logBackupEntry(prefKey, true) + } + + private fun addBackupEntryInt( + outPrefList: MutableList, + prefs: SharedPreferences, + prefFileName: String, + prefKey: String, + ) { + val value = prefs.getInt(prefKey, -1) + if (value == -1) { + logBackupEntry(prefKey, false) + return + } + outPrefList.add(BackupProtos.SharedPreference.newBuilder() + .setFile(prefFileName) + .setKey(PREF_PREFIX_TYPE_INT + prefKey) // The prefix denotes the type of the preference. + .setValue(value.toString()) + .build()) + logBackupEntry(prefKey, true) + } + + private fun addBackupEntryBoolean( + outPrefList: MutableList, + prefs: SharedPreferences, + prefFileName: String, + prefKey: String, + ) { + if (!prefs.contains(prefKey)) { + logBackupEntry(prefKey, false) + return + } + outPrefList.add(BackupProtos.SharedPreference.newBuilder() + .setFile(prefFileName) + .setKey(PREF_PREFIX_TYPE_BOOLEAN + prefKey) // The prefix denotes the type of the preference. + .setValue(prefs.getBoolean(prefKey, false).toString()) + .build()) + logBackupEntry(prefKey, true) + } + + private fun logBackupEntry(prefName: String, wasIncluded: Boolean) { + val sb = StringBuilder() + sb.append("Backup preference ") + sb.append(if (wasIncluded) "+ " else "- ") + sb.append('\"').append(prefName).append("\" ") + if (!wasIncluded) { + sb.append("(is empty and not included)") + } + Log.d(TAG, sb.toString()) + } // endregion + */ + + // NEVER rename these -- they're persisted by name + enum class MediaKeyboardMode { + EMOJI, STICKER + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/Util.kt b/libsession/src/main/java/org/session/libsession/utilities/Util.kt new file mode 100644 index 0000000000..0350307423 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/Util.kt @@ -0,0 +1,13 @@ +package org.session.libsession.utilities + +object Util { + fun join(list: Collection, delimiter: String?): String { + val result = StringBuilder() + var i = 0 + for (item in list) { + result.append(item) + if (++i < list.size) result.append(delimiter) + } + return result.toString() + } +} \ No newline at end of file