Merge branch 'dev' into security

This commit is contained in:
Niels Andriesse
2021-07-12 14:27:14 +10:00
536 changed files with 11430 additions and 14623 deletions

View File

@@ -6,6 +6,10 @@ plugins {
android {
compileSdkVersion androidCompileSdkVersion
defaultConfig {
minSdkVersion androidMinimumSdkVersion
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8

View File

@@ -3,6 +3,7 @@ package org.session.libsession.database
import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.UploadResult
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceAttachmentStream
import java.io.InputStream
@@ -18,9 +19,11 @@ interface MessageDataProvider {
fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: AttachmentId, messageID: Long)
fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream)
fun isOutgoingMessage(timestamp: Long): Boolean
fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long, threadId: Long)
fun isMmsOutgoing(mmsMessageId: Long): Boolean
fun isOutgoingMessage(mmsId: Long): Boolean
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
fun handleFailedAttachmentUpload(attachmentId: Long)
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
@@ -28,4 +31,5 @@ interface MessageDataProvider {
fun getMessageBodyFor(timestamp: Long, author: String): String
fun getAttachmentIDsFor(messageID: Long): List<Long>
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
fun getIndividualRecipientForMms(mmsId: Long): Recipient?
}

View File

@@ -88,6 +88,7 @@ interface StorageProtocol {
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
fun markAsSending(timestamp: Long, author: String)
fun markAsSent(timestamp: Long, author: String)
fun markUnidentified(timestamp: Long, author: String)
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
@@ -137,6 +138,7 @@ interface StorageProtocol {
fun getContactWithSessionID(sessionID: String): Contact?
fun getAllContacts(): Set<Contact>
fun setContact(contact: Contact)
fun getRecipientForThread(threadId: Long): Recipient?
fun getRecipientSettings(address: Address): RecipientSettings?
fun addContacts(contacts: List<ConfigurationMessage.Contact>)

View File

@@ -1,24 +1,27 @@
package org.session.libsession.messaging
import android.content.Context
import com.goterl.lazysodium.utils.KeyPair
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
class MessagingModuleConfiguration(
val context: Context,
val storage: StorageProtocol,
val messageDataProvider: MessageDataProvider
val messageDataProvider: MessageDataProvider,
val keyPairProvider: ()-> KeyPair?
) {
companion object {
lateinit var shared: MessagingModuleConfiguration
fun configure(context: Context,
storage: StorageProtocol,
messageDataProvider: MessageDataProvider
storage: StorageProtocol,
messageDataProvider: MessageDataProvider,
keyPairProvider: () -> KeyPair?
) {
if (Companion::shared.isInitialized) { return }
shared = MessagingModuleConfiguration(context, storage, messageDataProvider)
shared = MessagingModuleConfiguration(context, storage, messageDataProvider, keyPairProvider)
}
}
}

View File

@@ -77,11 +77,7 @@ class Contact(val sessionID: String) {
companion object {
fun contextForRecipient(recipient: Recipient): ContactContext {
return if (recipient.isOpenGroupRecipient) {
ContactContext.OPEN_GROUP
} else {
ContactContext.REGULAR
}
return if (recipient.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
}
}
}

View File

@@ -3,14 +3,21 @@ package org.session.libsession.messaging.jobs
import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.DecodedAudio
import org.session.libsession.utilities.DownloadUtilities
import org.session.libsession.utilities.InputStreamMediaDataSource
import org.session.libsignal.streams.AttachmentCipherInputStream
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.lang.NullPointerException
class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) : Job {
override var delegate: JobDelegate? = null
@@ -20,10 +27,13 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
// Error
internal sealed class Error(val description: String) : Exception(description) {
object NoAttachment : Error("No such attachment.")
object NoThread: Error("Thread no longer exists")
object NoSender: Error("Thread recipient or sender does not exist")
object DuplicateData: Error("Attachment already downloaded")
}
// Settings
override val maxFailureCount: Int = 20
override val maxFailureCount: Int = 100
companion object {
val KEY: String = "AttachmentDownloadJob"
@@ -36,47 +46,103 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
override fun execute() {
val storage = MessagingModuleConfiguration.shared.storage
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
if (exception == Error.NoAttachment) {
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
val threadID = storage.getThreadIdForMms(databaseMessageID)
val handleFailure: (java.lang.Exception, attachmentId: AttachmentId?) -> Unit = { exception, attachment ->
if (exception == Error.NoAttachment
|| exception == Error.NoThread
|| exception == Error.NoSender
|| (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 400)) {
attachment?.let { id ->
messageDataProvider.setAttachmentState(AttachmentState.FAILED, id, databaseMessageID)
} ?: run {
messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID)
}
this.handlePermanentFailure(exception)
} else {
this.handleFailure(exception)
}
}
if (threadID < 0) {
handleFailure(Error.NoThread, null)
return
}
val threadRecipient = storage.getRecipientForThread(threadID)
val sender = if (messageDataProvider.isMmsOutgoing(databaseMessageID)) {
storage.getUserPublicKey()
} else {
messageDataProvider.getIndividualRecipientForMms(databaseMessageID)?.address?.serialize()
}
val contact = sender?.let { storage.getContactWithSessionID(it) }
if (threadRecipient == null || sender == null || contact == null) {
handleFailure(Error.NoSender, null)
return
}
if (!threadRecipient.isGroupRecipient && (!contact.isTrusted && storage.getUserPublicKey() != sender)) {
// if we aren't receiving a group message, a message from ourselves (self-send) and the contact sending is not trusted:
// do not continue, but do not fail
return
}
var tempFile: File? = null
try {
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
?: return handleFailure(Error.NoAttachment)
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
val tempFile = createTempFile()
val threadID = storage.getThreadIdForMms(databaseMessageID)
?: return handleFailure(Error.NoAttachment, null)
if (attachment.hasData()) {
handleFailure(Error.DuplicateData, attachment.attachmentId)
return
}
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachment.attachmentId, this.databaseMessageID)
tempFile = createTempFile()
val openGroupV2 = storage.getV2OpenGroup(threadID)
val inputStream = if (openGroupV2 == null) {
if (openGroupV2 == null) {
DownloadUtilities.downloadFile(tempFile, attachment.url)
// Assume we're retrieving an attachment for an open group server if the digest is not set
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
FileInputStream(tempFile)
} else {
AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
}
} else {
val url = HttpUrl.parse(attachment.url)!!
val fileID = url.pathSegments().last()
OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let {
tempFile.writeBytes(it)
}
FileInputStream(tempFile)
}
val inputStream = getInputStream(tempFile, attachment)
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, inputStream)
if (attachment.contentType.startsWith("audio/")) {
// process the duration
try {
InputStreamMediaDataSource(getInputStream(tempFile, attachment)).use { mediaDataSource ->
val durationMs = (DecodedAudio.create(mediaDataSource).totalDuration / 1000.0).toLong()
messageDataProvider.updateAudioAttachmentDuration(
attachment.attachmentId,
durationMs,
threadID
)
}
} catch (e: Exception) {
Log.e("Loki", "Couldn't process audio attachment", e)
}
}
tempFile.delete()
handleSuccess()
} catch (e: Exception) {
return handleFailure(e)
tempFile?.delete()
return handleFailure(e,null)
}
}
private fun getInputStream(tempFile: File, attachment: DatabaseAttachment): InputStream {
// Assume we're retrieving an attachment for an open group server if the digest is not set
return if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
FileInputStream(tempFile)
} else {
AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
}
}
private fun handleSuccess() {
Log.w(AttachmentUploadJob.TAG, "Attachment downloaded successfully.")
Log.w("AttachmentDownloadJob", "Attachment downloaded successfully.")
delegate?.handleJobSucceeded(this)
}

View File

@@ -11,16 +11,14 @@ import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.DecodedAudio
import org.session.libsession.utilities.InputStreamMediaDataSource
import org.session.libsession.utilities.UploadResult
import org.session.libsignal.streams.AttachmentCipherOutputStream
import org.session.libsignal.messages.SignalServiceAttachmentStream
import org.session.libsignal.streams.PaddingInputStream
import org.session.libsignal.utilities.PushAttachmentData
import org.session.libsignal.streams.AttachmentCipherOutputStreamFactory
import org.session.libsignal.streams.DigestingRequestBody
import org.session.libsignal.utilities.Util
import org.session.libsignal.streams.PlaintextOutputStreamFactory
import org.session.libsignal.streams.*
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.PushAttachmentData
import org.session.libsignal.utilities.Util
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
override var delegate: JobDelegate? = null
@@ -108,7 +106,22 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
Log.d(TAG, "Attachment uploaded successfully.")
delegate?.handleJobSucceeded(this)
MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
if (attachment.contentType.startsWith("audio/")) {
// process the duration
try {
val inputStream = messageDataProvider.getAttachmentStream(attachmentID)!!.inputStream!!
InputStreamMediaDataSource(inputStream).use { mediaDataSource ->
val durationMs = (DecodedAudio.create(mediaDataSource).totalDuration / 1000.0).toLong()
messageDataProvider.getDatabaseAttachment(attachmentID)?.attachmentId?.let { attachmentId ->
messageDataProvider.updateAudioAttachmentDuration(attachmentId, durationMs, threadID.toLong())
}
}
} catch (e: Exception) {
Log.e("Loki", "Couldn't process audio attachment", e)
}
}
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
}
@@ -122,7 +135,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
private fun handleFailure(e: Exception) {
Log.w(TAG, "Attachment upload failed due to error: $this.")
delegate?.handleJobFailed(this, e)
if (failureCount + 1 == maxFailureCount) {
if (failureCount + 1 >= maxFailureCount) {
failAssociatedMessageSendJob(e)
}
}
@@ -140,13 +153,13 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
val kryo = Kryo()
kryo.isRegistrationRequired = false
val serializedMessage = ByteArray(4096)
val output = Output(serializedMessage)
kryo.writeObject(output, message)
val output = Output(serializedMessage, Job.MAX_BUFFER_SIZE)
kryo.writeClassAndObject(output, message)
output.close()
return Data.Builder()
.putLong(ATTACHMENT_ID_KEY, attachmentID)
.putString(THREAD_ID_KEY, threadID)
.putByteArray(MESSAGE_KEY, serializedMessage)
.putByteArray(MESSAGE_KEY, output.toBytes())
.putString(MESSAGE_SEND_JOB_ID_KEY, messageSendJobID)
.build()
}
@@ -157,18 +170,24 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
class Factory: Job.Factory<AttachmentUploadJob> {
override fun create(data: Data): AttachmentUploadJob {
override fun create(data: Data): AttachmentUploadJob? {
val serializedMessage = data.getByteArray(MESSAGE_KEY)
val kryo = Kryo()
kryo.isRegistrationRequired = false
val input = Input(serializedMessage)
val message = kryo.readObject(input, Message::class.java)
val message: Message
try {
message = kryo.readClassAndObject(input) as Message
} catch (e: Exception) {
Log.e("Loki","Couldn't serialize the AttachmentUploadJob", e)
return null
}
input.close()
return AttachmentUploadJob(
data.getLong(ATTACHMENT_ID_KEY),
data.getString(THREAD_ID_KEY)!!,
message,
data.getString(MESSAGE_SEND_JOB_ID_KEY)!!
data.getLong(ATTACHMENT_ID_KEY),
data.getString(THREAD_ID_KEY)!!,
message,
data.getString(MESSAGE_SEND_JOB_ID_KEY)!!
)
}
}

View File

@@ -20,9 +20,10 @@ class JobQueue : JobDelegate {
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val attachmentDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
private val attachmentDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
private val scope = GlobalScope + SupervisorJob()
private val queue = Channel<Job>(UNLIMITED)
private val pendingJobIds = mutableSetOf<String>()
val timer = Timer()
@@ -86,6 +87,36 @@ class JobQueue : JobDelegate {
MessagingModuleConfiguration.shared.storage.persistJob(job)
}
fun resumePendingSendMessage(job: Job) {
val id = job.id ?: run {
Log.e("Loki", "tried to resume pending send job with no ID")
return
}
if (!pendingJobIds.add(id)) {
Log.e("Loki","tried to re-queue pending/in-progress job")
return
}
queue.offer(job)
Log.d("Loki", "resumed pending send message $id")
}
fun resumePendingJobs(typeKey: String) {
val allPendingJobs = MessagingModuleConfiguration.shared.storage.getAllPendingJobs(typeKey)
val pendingJobs = mutableListOf<Job>()
for ((id, job) in allPendingJobs) {
if (job == null) {
// Job failed to deserialize, remove it from the DB
handleJobFailedPermanently(id)
} else {
pendingJobs.add(job)
}
}
pendingJobs.sortedBy { it.id }.forEach { job ->
Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.")
queue.offer(job) // Offer always called on unlimited capacity
}
}
fun resumePendingJobs() {
if (hasResumedPendingJobs) {
Log.d("Loki", "resumePendingJobs() should only be called once.")
@@ -100,26 +131,14 @@ class JobQueue : JobDelegate {
NotifyPNServerJob.KEY
)
allJobTypes.forEach { type ->
val allPendingJobs = MessagingModuleConfiguration.shared.storage.getAllPendingJobs(type)
val pendingJobs = mutableListOf<Job>()
for ((id, job) in allPendingJobs) {
if (job == null) {
// Job failed to deserialize, remove it from the DB
handleJobFailedPermanently(id)
} else {
pendingJobs.add(job)
}
}
pendingJobs.sortedBy { it.id }.forEach { job ->
Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.")
queue.offer(job) // Offer always called on unlimited capacity
}
resumePendingJobs(type)
}
}
override fun handleJobSucceeded(job: Job) {
val jobId = job.id ?: return
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId)
pendingJobIds.remove(jobId)
}
override fun handleJobFailed(job: Job, error: Exception) {
@@ -169,4 +188,7 @@ class JobQueue : JobDelegate {
val maxBackoff = (10 * 60).toDouble() // 10 minutes
return (1000 * 0.25 * min(maxBackoff, (2.0).pow(job.failureCount))).roundToLong()
}
private fun Job.isSend() = this is MessageSendJob || this is AttachmentUploadJob
}

View File

@@ -3,6 +3,8 @@ package org.session.libsession.messaging.jobs
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import nl.komponents.kovenant.FailedException
import nl.komponents.kovenant.Promise
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
import org.session.libsession.messaging.messages.Destination
@@ -34,6 +36,14 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
override fun execute() {
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val message = message as? VisibleMessage
val storage = MessagingModuleConfiguration.shared.storage
val sentTimestamp = this.message.sentTimestamp
val sender = storage.getUserPublicKey()
if (sentTimestamp != null && sender != null) {
storage.markAsSending(sentTimestamp, sender)
}
if (message != null) {
if (!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
val attachmentIDs = mutableListOf<Long>()
@@ -43,7 +53,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
val attachments = attachmentIDs.mapNotNull { messageDataProvider.getDatabaseAttachment(it) }
val attachmentsToUpload = attachments.filter { it.url.isNullOrEmpty() }
attachmentsToUpload.forEach {
if (MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId.rowId) != null) {
if (storage.getAttachmentUploadJob(it.attachmentId.rowId) != null) {
// Wait for it to finish
} else {
val job = AttachmentUploadJob(it.attachmentId.rowId, message.threadID!!.toString(), message, id!!)
@@ -55,7 +65,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
return
} // Wait for all attachments to upload before continuing
}
MessageSender.send(this.message, this.destination).success {
val promise = MessageSender.send(this.message, this.destination).success {
this.handleSuccess()
}.fail { exception ->
Log.e(TAG, "Couldn't send message due to error: $exception.")
@@ -64,6 +74,11 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
}
this.handleFailure(exception)
}
try {
promise.get()
} catch (e: Exception) {
Log.d(TAG, "Promise failed to resolve successfully", e)
}
}
private fun handleSuccess() {

View File

@@ -7,6 +7,7 @@ import nl.komponents.kovenant.functional.map
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.utilities.Data
@@ -64,7 +65,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
val kryo = Kryo()
kryo.isRegistrationRequired = false
val serializedMessage = ByteArray(4096)
val output = Output(serializedMessage)
val output = Output(serializedMessage, MAX_BUFFER_SIZE)
kryo.writeObject(output, message)
output.close()
return Data.Builder()

View File

@@ -34,7 +34,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
is Kind.New -> {
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair?.publicKey != null
&& kind.encryptionKeyPair?.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
&& kind.expireTimer >= 0
&& kind.expirationTimer >= 0
}
is Kind.EncryptionKeyPair -> true
is Kind.NameChange -> kind.name.isNotEmpty()
@@ -45,7 +45,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
}
sealed class Kind {
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>, var expireTimer: Int) : Kind() {
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>, var expirationTimer: Int) : Kind() {
internal constructor() : this(ByteString.EMPTY, "", null, listOf(), listOf(), 0)
}
/** An encryption key pair encrypted for each member individually.
@@ -89,11 +89,11 @@ class ClosedGroupControlMessage() : ControlMessage() {
val publicKey = closedGroupControlMessageProto.publicKey ?: return null
val name = closedGroupControlMessageProto.name ?: return null
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
val expireTimer = closedGroupControlMessageProto.expireTimer
val expirationTimer = closedGroupControlMessageProto.expirationTimer
try {
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()),
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList, expireTimer)
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList, expirationTimer)
} catch (e: Exception) {
Log.w(TAG, "Couldn't parse key pair from proto: $encryptionKeyPairAsProto.")
return null
@@ -145,7 +145,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
closedGroupControlMessage.addAllMembers(kind.members)
closedGroupControlMessage.addAllAdmins(kind.admins)
closedGroupControlMessage.expireTimer = kind.expireTimer
closedGroupControlMessage.expirationTimer = kind.expirationTimer
}
is Kind.EncryptionKeyPair -> {
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
@@ -173,10 +173,6 @@ class ClosedGroupControlMessage() : ControlMessage() {
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
// Group context
setGroupContext(dataMessageProto)
// Expiration timer
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
// if it receives a message without the current expiration timer value attached to it...
dataMessageProto.expireTimer = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
contentProto.dataMessage = dataMessageProto.build()
return contentProto.build()
} catch (e: Exception) {

View File

@@ -6,6 +6,7 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
@@ -19,10 +20,10 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
override val isSelfSendValid: Boolean = true
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>, var expirationTimer: Int) {
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
internal constructor() : this("", "", null, listOf(), listOf())
internal constructor() : this("", "", null, listOf(), listOf(), 0)
override fun toString(): String {
return name
@@ -39,7 +40,8 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
val members = proto.membersList.map { it.toByteArray().toHexString() }
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
val expirationTimer = proto.expirationTimer
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins, expirationTimer)
}
}
@@ -53,6 +55,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
result.expirationTimer = expirationTimer
return result.build()
}
}
@@ -110,7 +113,8 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString()
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
val closedGroup = ClosedGroup(groupPublicKey, group.title, encryptionKeyPair, group.members.map { it.serialize() }, group.admins.map { it.serialize() })
val recipient = Recipient.from(context, Address.fromSerialized(group.encodedId), false)
val closedGroup = ClosedGroup(groupPublicKey, group.title, encryptionKeyPair, group.members.map { it.serialize() }, group.admins.map { it.serialize() }, recipient.expireMessages)
closedGroups.add(closedGroup)
}
if (group.isOpenGroup) {

View File

@@ -34,7 +34,7 @@ class DataExtractionNotification() : ControlMessage() {
}
}
internal constructor(kind: Kind) : this() {
constructor(kind: Kind) : this() {
this.kind = kind
}

View File

@@ -33,7 +33,7 @@ class ExpirationTimerUpdate() : ControlMessage() {
}
}
internal constructor(duration: Int) : this() {
constructor(duration: Int) : this() {
this.syncTarget = null
this.duration = duration
}

View File

@@ -6,7 +6,6 @@ import com.goterl.lazysodium.interfaces.Box
import com.goterl.lazysodium.interfaces.Sign
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.utilities.KeyPairUtilities
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.removing05PrefixIfNeeded
@@ -25,7 +24,7 @@ object MessageEncrypter {
*/
internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
val context = MessagingModuleConfiguration.shared.context
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
val userED25519KeyPair = MessagingModuleConfiguration.shared.keyPairProvider() ?: throw Error.NoUserED25519KeyPair
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey

View File

@@ -121,7 +121,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
for (closedGroup in message.closedGroups) {
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!, 0)
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!, closedGroup.expirationTimer)
}
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
for (openGroup in message.openGroups) {
@@ -256,7 +256,7 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
val members = kind.members.map { it.toByteArray().toHexString() }
val admins = kind.admins.map { it.toByteArray().toHexString() }
val expireTimer = kind.expireTimer
val expireTimer = kind.expirationTimer
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expireTimer)
}

View File

@@ -94,7 +94,9 @@ class OpenGroupPollerV2(private val server: String, private val executorService:
if (actualMax > 0) {
storage.setLastMessageServerID(room, server, actualMax)
}
JobQueue.shared.add(TrimThreadJob(threadId))
if (messages.isNotEmpty()) {
JobQueue.shared.add(TrimThreadJob(threadId))
}
}
private fun handleDeletedMessages(room: String, openGroupID: String, deletions: List<OpenGroupAPIV2.MessageDeletion>) {

View File

@@ -27,6 +27,7 @@ private typealias Path = List<Snode>
* See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
*/
object OnionRequestAPI {
private var buildPathsPromise: Promise<List<Path>, Exception>? = null
private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
private val broadcaster: Broadcaster
@@ -158,9 +159,11 @@ object OnionRequestAPI {
* enough (reliable) snodes are available.
*/
private fun buildPaths(reusablePaths: List<Path>): Promise<List<Path>, Exception> {
val existingBuildPathsPromise = buildPathsPromise
if (existingBuildPathsPromise != null) { return existingBuildPathsPromise }
Log.d("Loki", "Building onion request paths.")
broadcaster.broadcast("buildingPaths")
return SnodeAPI.getRandomSnode().bind { // Just used to populate the snode pool
val promise = SnodeAPI.getRandomSnode().bind { // Just used to populate the snode pool
val reusableGuardSnodes = reusablePaths.map { it[0] }
getGuardSnodes(reusableGuardSnodes).map { guardSnodes ->
var unusedSnodes = SnodeAPI.snodePool.minus(guardSnodes).minus(reusablePaths.flatten())
@@ -183,6 +186,10 @@ object OnionRequestAPI {
paths
}
}
promise.success { buildPathsPromise = null }
promise.fail { buildPathsPromise = null }
buildPathsPromise = promise
return promise
}
/**
@@ -433,11 +440,12 @@ object OnionRequestAPI {
internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String? = null): Promise<Map<*, *>, Exception> {
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
return sendOnionRequest(Destination.Snode(snode), payload).recover { exception ->
val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException
if (httpRequestFailedException != null) {
val error = SnodeAPI.handleSnodeError(httpRequestFailedException.statusCode, httpRequestFailedException.json, snode, publicKey)
if (error != null) { throw error }
val error = when (exception) {
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
is HTTPRequestFailedAtDestinationException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
else -> null
}
if (error != null) { throw error }
throw exception
}
}

View File

@@ -17,15 +17,11 @@ import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.utilities.IdentityKeyUtil
import org.session.libsession.utilities.KeyPairUtilities
import org.session.libsignal.crypto.getRandomElement
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.*
import org.session.libsignal.utilities.Base64
import org.whispersystems.curve25519.Curve25519
import java.nio.charset.Charset
import java.security.SecureRandom
import java.util.*
import kotlin.Pair
@@ -175,7 +171,7 @@ object SnodeAPI {
val validationCount = 3
val sessionIDByteCount = 33
// Hash the ONS name using BLAKE2b
val onsName = onsName.toLowerCase(Locale.ENGLISH)
val onsName = onsName.toLowerCase(Locale.US)
val nameAsData = onsName.toByteArray()
val nameHash = ByteArray(GenericHash.BYTES)
if (!sodium.cryptoGenericHash(nameHash, nameHash.size, nameAsData, nameAsData.size.toLong())) {
@@ -304,9 +300,7 @@ object SnodeAPI {
getTargetSnodes(destination).map { swarm ->
swarm.map { snode ->
val parameters = message.toJSON()
retryIfNeeded(maxRetryCount) {
invoke(Snode.Method.SendMessage, snode, destination, parameters)
}
invoke(Snode.Method.SendMessage, snode, destination, parameters)
}.toSet()
}
}
@@ -341,8 +335,9 @@ object SnodeAPI {
return retryIfNeeded(maxRetryCount) {
// considerations: timestamp off in retrying logic, not being able to re-sign with latest timestamp? do we just not retry this as it will be synchronous
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val module = MessagingModuleConfiguration.shared
val userED25519KeyPair = module.keyPairProvider() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.Generic)
getSingleTargetSnode(userPublicKey).bind { snode ->
retryIfNeeded(maxRetryCount) {

View File

@@ -8,7 +8,7 @@ data class SnodeMessage(
*/
val recipient: String,
/**
* The content of the message.
* The base64 encoded content of the message.
*/
val data: String,
/**

View File

@@ -0,0 +1,399 @@
package org.session.libsession.utilities
import android.media.AudioFormat
import android.media.MediaCodec
import android.media.MediaDataSource
import android.media.MediaExtractor
import android.media.MediaFormat
import android.os.Build
import androidx.annotation.RequiresApi
import java.io.FileDescriptor
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.ShortBuffer
import kotlin.jvm.Throws
import kotlin.math.ceil
import kotlin.math.roundToInt
import kotlin.math.sqrt
/**
* Decodes the audio data and provides access to its sample data.
* We need this to extract RMS values for waveform visualization.
*
* Use static [DecodedAudio.create] methods to instantiate a [DecodedAudio].
*
* Partially based on the old [Google's Ringdroid project]
* (https://github.com/google/ringdroid/blob/master/app/src/main/java/com/ringdroid/soundfile/SoundFile.java).
*
* *NOTE:* This class instance creation might be pretty slow (depends on the source audio file size).
* It's recommended to instantiate it in the background.
*/
@Suppress("MemberVisibilityCanBePrivate")
class DecodedAudio {
companion object {
@JvmStatic
@Throws(IOException::class)
fun create(fd: FileDescriptor, startOffset: Long, size: Long): DecodedAudio {
val mediaExtractor = MediaExtractor().apply { setDataSource(fd, startOffset, size) }
return DecodedAudio(mediaExtractor, size)
}
@JvmStatic
@RequiresApi(api = Build.VERSION_CODES.M)
@Throws(IOException::class)
fun create(dataSource: MediaDataSource): DecodedAudio {
val mediaExtractor = MediaExtractor().apply { setDataSource(dataSource) }
return DecodedAudio(mediaExtractor, dataSource.size)
}
}
val dataSize: Long
/** Average bit rate in kbps. */
val avgBitRate: Int
val sampleRate: Int
/** In microseconds. */
val totalDuration: Long
val channels: Int
/** Total number of samples per channel in audio file. */
val numSamples: Int
val samples: ShortBuffer
get() {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1
) {
// Hack for Nougat where asReadOnlyBuffer fails to respect byte ordering.
// See https://code.google.com/p/android/issues/detail?id=223824
decodedSamples
} else {
decodedSamples.asReadOnlyBuffer()
}
}
/**
* Shared buffer with mDecodedBytes.
* Has the following format:
* {s1c1, s1c2, ..., s1cM, s2c1, ..., s2cM, ..., sNc1, ..., sNcM}
* where sicj is the ith sample of the jth channel (a sample is a signed short)
* M is the number of channels (e.g. 2 for stereo) and N is the number of samples per channel.
*/
private val decodedSamples: ShortBuffer
@Throws(IOException::class)
private constructor(extractor: MediaExtractor, size: Long) {
dataSize = size
var mediaFormat: MediaFormat? = null
// Find and select the first audio track present in the file.
for (trackIndex in 0 until extractor.trackCount) {
val format = extractor.getTrackFormat(trackIndex)
if (format.getString(MediaFormat.KEY_MIME)!!.startsWith("audio/")) {
extractor.selectTrack(trackIndex)
mediaFormat = format
break
}
}
if (mediaFormat == null) {
throw IOException("No audio track found in the data source.")
}
channels = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
// On some old APIs (23) this field might be missing.
totalDuration = if (mediaFormat.containsKey(MediaFormat.KEY_DURATION)) {
mediaFormat.getLong(MediaFormat.KEY_DURATION)
} else {
-1L
}
// Expected total number of samples per channel.
val expectedNumSamples = if (totalDuration >= 0) {
((totalDuration / 1000000f) * sampleRate + 0.5f).toInt()
} else {
Int.MAX_VALUE
}
val codec = MediaCodec.createDecoderByType(mediaFormat.getString(MediaFormat.KEY_MIME)!!)
codec.configure(mediaFormat, null, null, 0)
codec.start()
// Check if the track is in PCM 16 bit encoding.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
val pcmEncoding = codec.outputFormat.getInteger(MediaFormat.KEY_PCM_ENCODING)
if (pcmEncoding != AudioFormat.ENCODING_PCM_16BIT) {
throw IOException("Unsupported PCM encoding code: $pcmEncoding")
}
} catch (e: NullPointerException) {
// If KEY_PCM_ENCODING is not specified, means it's ENCODING_PCM_16BIT.
}
}
var decodedSamplesSize: Int = 0 // size of the output buffer containing decoded samples.
var decodedSamples: ByteArray? = null
var sampleSize: Int
val info = MediaCodec.BufferInfo()
var presentationTime: Long
var totalSizeRead: Int = 0
var doneReading = false
// Set the size of the decoded samples buffer to 1MB (~6sec of a stereo stream at 44.1kHz).
// For longer streams, the buffer size will be increased later on, calculating a rough
// estimate of the total size needed to store all the samples in order to resize the buffer
// only once.
var decodedBytes: ByteBuffer = ByteBuffer.allocate(1 shl 20)
var firstSampleData = true
while (true) {
// read data from file and feed it to the decoder input buffers.
val inputBufferIndex: Int = codec.dequeueInputBuffer(100)
if (!doneReading && inputBufferIndex >= 0) {
sampleSize = extractor.readSampleData(codec.getInputBuffer(inputBufferIndex)!!, 0)
if (firstSampleData
&& mediaFormat.getString(MediaFormat.KEY_MIME)!! == "audio/mp4a-latm"
&& sampleSize == 2
) {
// For some reasons on some devices (e.g. the Samsung S3) you should not
// provide the first two bytes of an AAC stream, otherwise the MediaCodec will
// crash. These two bytes do not contain music data but basic info on the
// stream (e.g. channel configuration and sampling frequency), and skipping them
// seems OK with other devices (MediaCodec has already been configured and
// already knows these parameters).
extractor.advance()
totalSizeRead += sampleSize
} else if (sampleSize < 0) {
// All samples have been read.
codec.queueInputBuffer(
inputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
doneReading = true
} else {
presentationTime = extractor.sampleTime
codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTime, 0)
extractor.advance()
totalSizeRead += sampleSize
}
firstSampleData = false
}
// Get decoded stream from the decoder output buffers.
val outputBufferIndex: Int = codec.dequeueOutputBuffer(info, 100)
if (outputBufferIndex >= 0 && info.size > 0) {
if (decodedSamplesSize < info.size) {
decodedSamplesSize = info.size
decodedSamples = ByteArray(decodedSamplesSize)
}
val outputBuffer: ByteBuffer = codec.getOutputBuffer(outputBufferIndex)!!
outputBuffer.get(decodedSamples!!, 0, info.size)
outputBuffer.clear()
// Check if buffer is big enough. Resize it if it's too small.
if (decodedBytes.remaining() < info.size) {
// Getting a rough estimate of the total size, allocate 20% more, and
// make sure to allocate at least 5MB more than the initial size.
val position = decodedBytes.position()
var newSize = ((position * (1.0 * dataSize / totalSizeRead)) * 1.2).toInt()
if (newSize - position < info.size + 5 * (1 shl 20)) {
newSize = position + info.size + 5 * (1 shl 20)
}
var newDecodedBytes: ByteBuffer? = null
// Try to allocate memory. If we are OOM, try to run the garbage collector.
var retry = 10
while (retry > 0) {
try {
newDecodedBytes = ByteBuffer.allocate(newSize)
break
} catch (e: OutOfMemoryError) {
// setting android:largeHeap="true" in <application> seem to help not
// reaching this section.
retry--
}
}
if (retry == 0) {
// Failed to allocate memory... Stop reading more data and finalize the
// instance with the data decoded so far.
break
}
decodedBytes.rewind()
newDecodedBytes!!.put(decodedBytes)
decodedBytes = newDecodedBytes
decodedBytes.position(position)
}
decodedBytes.put(decodedSamples, 0, info.size)
codec.releaseOutputBuffer(outputBufferIndex, false)
}
if ((info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
|| (decodedBytes.position() / (2 * channels)) >= expectedNumSamples
) {
// We got all the decoded data from the decoder. Stop here.
// Theoretically dequeueOutputBuffer(info, ...) should have set info.flags to
// MediaCodec.BUFFER_FLAG_END_OF_STREAM. However some phones (e.g. Samsung S3)
// won't do that for some files (e.g. with mono AAC files), in which case subsequent
// calls to dequeueOutputBuffer may result in the application crashing, without
// even an exception being thrown... Hence the second check.
// (for mono AAC files, the S3 will actually double each sample, as if the stream
// was stereo. The resulting stream is half what it's supposed to be and with a much
// lower pitch.)
break
}
}
numSamples = decodedBytes.position() / (channels * 2) // One sample = 2 bytes.
decodedBytes.rewind()
decodedBytes.order(ByteOrder.LITTLE_ENDIAN)
this.decodedSamples = decodedBytes.asShortBuffer()
avgBitRate = ((dataSize * 8) * (sampleRate.toFloat() / numSamples) / 1000).toInt()
extractor.release()
codec.stop()
codec.release()
}
fun calculateRms(maxFrames: Int): ByteArray {
return calculateRms(this.samples, this.numSamples, this.channels, maxFrames)
}
}
/**
* Computes audio RMS values for the first channel only.
*
* A typical RMS calculation algorithm is:
* 1. Square each sample
* 2. Sum the squared samples
* 3. Divide the sum of the squared samples by the number of samples
* 4. Take the square root of step 3., the mean of the squared samples
*
* @param maxFrames Defines amount of output RMS frames.
* If number of samples per channel is less than "maxFrames",
* the result array will match the source sample size instead.
*
* @return normalized RMS values as a signed byte array.
*/
private fun calculateRms(samples: ShortBuffer, numSamples: Int, channels: Int, maxFrames: Int): ByteArray {
val numFrames: Int
val frameStep: Float
val samplesPerChannel = numSamples / channels
if (samplesPerChannel <= maxFrames) {
frameStep = 1f
numFrames = samplesPerChannel
} else {
frameStep = numSamples / maxFrames.toFloat()
numFrames = maxFrames
}
val rmsValues = FloatArray(numFrames)
var squaredFrameSum = 0.0
var currentFrameIdx = 0
fun calculateFrameRms(nextFrameIdx: Int) {
rmsValues[currentFrameIdx] = sqrt(squaredFrameSum.toFloat())
// Advance to the next frame.
squaredFrameSum = 0.0
currentFrameIdx = nextFrameIdx
}
(0 until numSamples * channels step channels).forEach { sampleIdx ->
val channelSampleIdx = sampleIdx / channels
val frameIdx = (channelSampleIdx / frameStep).toInt()
if (currentFrameIdx != frameIdx) {
// Calculate RMS value for the previous frame.
calculateFrameRms(frameIdx)
}
val samplesInCurrentFrame = ceil((currentFrameIdx + 1) * frameStep) - ceil(currentFrameIdx * frameStep)
squaredFrameSum += (samples[sampleIdx] * samples[sampleIdx]) / samplesInCurrentFrame
}
// Calculate RMS value for the last frame.
calculateFrameRms(-1)
// smoothArray(rmsValues, 1.0f)
normalizeArray(rmsValues)
// Convert normalized result to a signed byte array.
return rmsValues.map { value -> normalizedFloatToByte(value) }.toByteArray()
}
/**
* Normalizes the array's values to [0..1] range.
*/
private fun normalizeArray(values: FloatArray) {
var maxValue = -Float.MAX_VALUE
var minValue = +Float.MAX_VALUE
values.forEach { value ->
if (value > maxValue) maxValue = value
if (value < minValue) minValue = value
}
val span = maxValue - minValue
if (span == 0f) {
values.indices.forEach { i -> values[i] = 0f }
return
}
values.indices.forEach { i -> values[i] = (values[i] - minValue) / span }
}
private fun smoothArray(values: FloatArray, neighborWeight: Float = 1f): FloatArray {
if (values.size < 3) return values
val result = FloatArray(values.size)
result[0] = values[0]
result[values.size - 1] == values[values.size - 1]
for (i in 1 until values.size - 1) {
result[i] = (values[i] + values[i - 1] * neighborWeight +
values[i + 1] * neighborWeight) / (1f + neighborWeight * 2f)
}
return result
}
/** Turns a signed byte into a [0..1] float. */
inline fun byteToNormalizedFloat(value: Byte): Float {
return (value + 128f) / 255f
}
/** Turns a [0..1] float into a signed byte. */
inline fun normalizedFloatToByte(value: Float): Byte {
return (255f * value - 128f).roundToInt().toByte()
}
class InputStreamMediaDataSource: MediaDataSource {
private val data: ByteArray
constructor(inputStream: InputStream): super() {
this.data = inputStream.readBytes()
}
override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int {
val length: Int = data.size
if (position >= length) {
return -1 // -1 indicates EOF
}
var actualSize = size
if (position + size > length) {
actualSize -= (position + size - length).toInt()
}
System.arraycopy(data, position.toInt(), buffer, offset, actualSize)
return actualSize
}
override fun getSize(): Long {
return data.size.toLong()
}
override fun close() {
// We don't need to close the wrapped stream.
}
}

View File

@@ -1,111 +0,0 @@
/*
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.session.libsession.utilities;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import androidx.annotation.NonNull;
import org.session.libsignal.crypto.ecc.ECPublicKey;
import org.session.libsignal.crypto.IdentityKey;
import org.session.libsignal.crypto.IdentityKeyPair;
import org.session.libsignal.exceptions.InvalidKeyException;
import org.session.libsignal.crypto.ecc.Curve;
import org.session.libsignal.crypto.ecc.ECKeyPair;
import org.session.libsignal.crypto.ecc.ECPrivateKey;
import org.session.libsignal.utilities.Base64;
import java.io.IOException;
/**
* Utility class for working with identity keys.
*
* @author Moxie Marlinspike
*/
public class IdentityKeyUtil {
private static final String MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences";
@SuppressWarnings("unused")
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
public static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
public static final String LOKI_SEED = "loki_seed";
public static boolean hasIdentityKey(Context context) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
return
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
}
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF));
return new IdentityKey(publicKeyBytes, 0);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
IdentityKey publicKey = getIdentityKey(context);
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));
return new IdentityKeyPair(publicKey, privateKey);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static void generateIdentityKeyPair(@NonNull Context context) {
ECKeyPair keyPair = Curve.generateKeyPair();
ECPublicKey publicKey = keyPair.getPublicKey();
ECPrivateKey privateKey = keyPair.getPrivateKey();
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(publicKey.serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(privateKey.serialize()));
}
public static String retrieve(Context context, String key) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
return preferences.getString(key, null);
}
public static void save(Context context, String key, String value) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
Editor preferencesEditor = preferences.edit();
preferencesEditor.putString(key, value);
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
}
public static void delete(Context context, String key) {
context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0).edit().remove(key).commit();
}
}

View File

@@ -1,60 +0,0 @@
package org.session.libsession.utilities
import android.content.Context
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
object KeyPairUtilities {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
fun generate(): KeyPairGenerationResult {
val seed = sodium.randomBytesBuf(16)
try {
return generate(seed)
} catch (exception: Exception) {
return generate()
}
}
fun generate(seed: ByteArray): KeyPairGenerationResult {
val padding = ByteArray(16) { 0 }
val ed25519KeyPair = sodium.cryptoSignSeedKeypair(seed + padding)
val sodiumX25519KeyPair = sodium.convertKeyPairEd25519ToCurve25519(ed25519KeyPair)
val x25519KeyPair = ECKeyPair(DjbECPublicKey(sodiumX25519KeyPair.publicKey.asBytes), DjbECPrivateKey(sodiumX25519KeyPair.secretKey.asBytes))
return KeyPairGenerationResult(seed, ed25519KeyPair, x25519KeyPair)
}
fun store(context: Context, seed: ByteArray, ed25519KeyPair: KeyPair, x25519KeyPair: ECKeyPair) {
IdentityKeyUtil.save(context, IdentityKeyUtil.LOKI_SEED, Hex.toStringCondensed(seed))
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(x25519KeyPair.publicKey.serialize()))
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(x25519KeyPair.privateKey.serialize()))
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_PUBLIC_KEY, Base64.encodeBytes(ed25519KeyPair.publicKey.asBytes))
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_SECRET_KEY, Base64.encodeBytes(ed25519KeyPair.secretKey.asBytes))
}
fun hasV2KeyPair(context: Context): Boolean {
return (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) != null)
}
fun getUserED25519KeyPair(context: Context): KeyPair? {
val base64EncodedED25519PublicKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_PUBLIC_KEY) ?: return null
val base64EncodedED25519SecretKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) ?: return null
val ed25519PublicKey = Key.fromBytes(Base64.decode(base64EncodedED25519PublicKey))
val ed25519SecretKey = Key.fromBytes(Base64.decode(base64EncodedED25519SecretKey))
return KeyPair(ed25519PublicKey, ed25519SecretKey)
}
data class KeyPairGenerationResult(
val seed: ByteArray,
val ed25519KeyPair: KeyPair,
val x25519KeyPair: ECKeyPair
)
}

View File

@@ -22,13 +22,10 @@ object TextSecurePreferences {
val events get() = _events.asSharedFlow()
const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase"
const val THEME_PREF = "pref_theme"
const val LANGUAGE_PREF = "pref_language"
const val THREAD_TRIM_LENGTH = "pref_trim_length"
const val THREAD_TRIM_NOW = "pref_trim_now"
private const val LAST_VERSION_CODE_PREF = "last_version_code"
private const val LAST_EXPERIENCE_VERSION_PREF = "last_experience_version_code"
const val RINGTONE_PREF = "pref_key_ringtone"
const val VIBRATE_PREF = "pref_key_vibrate"
private const val NOTIFICATION_PREF = "pref_key_enable_notifications"
@@ -46,20 +43,15 @@ object TextSecurePreferences {
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 IN_THREAD_NOTIFICATION_PREF = "pref_key_inthread_notifications"
const val MESSAGE_BODY_TEXT_SIZE_PREF = "pref_message_body_text_size"
const val LOCAL_REGISTRATION_ID_PREF = "pref_local_registration_id"
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 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"
const val DIRECT_CAPTURE_CAMERA_ID = "pref_direct_capture_camera_id"
const val PROFILE_KEY_PREF = "pref_profile_key"
@@ -68,45 +60,33 @@ object TextSecurePreferences {
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 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"
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 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"
const val UNIVERSAL_UNIDENTIFIED_ACCESS = "pref_universal_unidentified_access"
const val TYPING_INDICATORS = "pref_typing_indicators"
const val LINK_PREVIEWS = "pref_link_previews"
private const val GIF_GRID_LAYOUT = "pref_gif_grid_layout"
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 LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
private const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
private const val LAST_OPEN_DATE = "pref_last_open_date"
@JvmStatic
@@ -338,7 +318,7 @@ object TextSecurePreferences {
@JvmStatic
fun setProfileAvatarId(context: Context, id: Int) {
setIntegerPrefrence(context, PROFILE_AVATAR_ID_PREF, id)
setIntegerPreference(context, PROFILE_AVATAR_ID_PREF, id)
}
@JvmStatic
@@ -367,7 +347,7 @@ object TextSecurePreferences {
@JvmStatic
fun setDirectCaptureCameraId(context: Context, value: Int) {
setIntegerPrefrence(context, DIRECT_CAPTURE_CAMERA_ID, value)
setIntegerPreference(context, DIRECT_CAPTURE_CAMERA_ID, value)
}
@JvmStatic
@@ -395,7 +375,7 @@ object TextSecurePreferences {
}
fun setLocalRegistrationId(context: Context, registrationId: Int) {
setIntegerPrefrence(context, LOCAL_REGISTRATION_ID_PREF, registrationId)
setIntegerPreference(context, LOCAL_REGISTRATION_ID_PREF, registrationId)
}
@JvmStatic
@@ -476,24 +456,11 @@ object TextSecurePreferences {
@Throws(IOException::class)
fun setLastVersionCode(context: Context, versionCode: Int) {
if (!setIntegerPrefrenceBlocking(context, LAST_VERSION_CODE_PREF, versionCode)) {
if (!setIntegerPreferenceBlocking(context, LAST_VERSION_CODE_PREF, versionCode)) {
throw IOException("couldn't write version code to sharedpreferences")
}
}
fun setLastExperienceVersionCode(context: Context, versionCode: Int) {
setIntegerPrefrence(context, LAST_EXPERIENCE_VERSION_PREF, versionCode)
}
fun getTheme(context: Context): String? {
return getStringPreference(context, THEME_PREF, "light")
}
@JvmStatic
fun isPushRegistered(context: Context): Boolean {
return getBooleanPreference(context, REGISTERED_GCM_PREF, false)
}
@JvmStatic
fun isPassphraseTimeoutEnabled(context: Context): Boolean {
return getBooleanPreference(context, PASSPHRASE_TIMEOUT_PREF, false)
@@ -601,19 +568,14 @@ object TextSecurePreferences {
return getStringSetPreference(context, key, HashSet(Arrays.asList(*context.resources.getStringArray(defaultValuesRes))))
}
@JvmStatic
fun setLogEncryptedSecret(context: Context, base64Secret: String?) {
setStringPreference(context, LOG_ENCRYPTED_SECRET, base64Secret)
}
@JvmStatic
fun getLogEncryptedSecret(context: Context): String? {
return getStringPreference(context, LOG_ENCRYPTED_SECRET, null)
}
@JvmStatic
fun setLogUnencryptedSecret(context: Context, base64Secret: String?) {
setStringPreference(context, LOG_UNENCRYPTED_SECRET, base64Secret)
fun setLogEncryptedSecret(context: Context, base64Secret: String?) {
setStringPreference(context, LOG_ENCRYPTED_SECRET, base64Secret)
}
@JvmStatic
@@ -621,6 +583,11 @@ object TextSecurePreferences {
return getStringPreference(context, LOG_UNENCRYPTED_SECRET, null)
}
@JvmStatic
fun setLogUnencryptedSecret(context: Context, base64Secret: String?) {
setStringPreference(context, LOG_UNENCRYPTED_SECRET, base64Secret)
}
@JvmStatic
fun getNotificationChannelVersion(context: Context): Int {
return getIntegerPreference(context, NOTIFICATION_CHANNEL_VERSION, 1)
@@ -628,7 +595,7 @@ object TextSecurePreferences {
@JvmStatic
fun setNotificationChannelVersion(context: Context, version: Int) {
setIntegerPrefrence(context, NOTIFICATION_CHANNEL_VERSION, version)
setIntegerPreference(context, NOTIFICATION_CHANNEL_VERSION, version)
}
@JvmStatic
@@ -638,12 +605,7 @@ object TextSecurePreferences {
@JvmStatic
fun setNotificationMessagesChannelVersion(context: Context, version: Int) {
setIntegerPrefrence(context, NOTIFICATION_MESSAGES_CHANNEL_VERSION, version)
}
@JvmStatic
fun setBooleanPreference(context: Context, key: String?, value: Boolean) {
getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply()
setIntegerPreference(context, NOTIFICATION_MESSAGES_CHANNEL_VERSION, version)
}
@JvmStatic
@@ -652,8 +614,8 @@ object TextSecurePreferences {
}
@JvmStatic
fun setStringPreference(context: Context, key: String?, value: String?) {
getDefaultSharedPreferences(context).edit().putString(key, value).apply()
fun setBooleanPreference(context: Context, key: String?, value: Boolean) {
getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply()
}
@JvmStatic
@@ -661,15 +623,20 @@ object TextSecurePreferences {
return getDefaultSharedPreferences(context).getString(key, defaultValue)
}
@JvmStatic
fun setStringPreference(context: Context, key: String?, value: String?) {
getDefaultSharedPreferences(context).edit().putString(key, value).apply()
}
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) {
private fun setIntegerPreference(context: Context, key: String, value: Int) {
getDefaultSharedPreferences(context).edit().putInt(key, value).apply()
}
private fun setIntegerPrefrenceBlocking(context: Context, key: String, value: Int): Boolean {
private fun setIntegerPreferenceBlocking(context: Context, key: String, value: Int): Boolean {
return getDefaultSharedPreferences(context).edit().putInt(key, value).commit()
}
@@ -694,9 +661,6 @@ object TextSecurePreferences {
}
}
// region Loki
@JvmStatic
fun getHasViewedSeed(context: Context): Boolean {
return getBooleanPreference(context, "has_viewed_seed", false)
}
@@ -723,21 +687,6 @@ object TextSecurePreferences {
setLongPreference(context, "last_profile_picture_upload", newValue)
}
@JvmStatic
fun hasSeenGIFMetaDataWarning(context: Context): Boolean {
return getBooleanPreference(context, "has_seen_gif_metadata_warning", false)
}
@JvmStatic
fun setHasSeenGIFMetaDataWarning(context: Context) {
setBooleanPreference(context, "has_seen_gif_metadata_warning", true)
}
@JvmStatic
fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit()
}
fun getLastSnodePoolRefreshDate(context: Context?): Long {
return getLongPreference(context!!, "last_snode_pool_refresh_date", 0)
}
@@ -746,13 +695,9 @@ object TextSecurePreferences {
setLongPreference(context!!, "last_snode_pool_refresh_date", date.time)
}
fun getIsMigratingKeyPair(context: Context?): Boolean {
return getBooleanPreference(context!!, "is_migrating_key_pair", false)
}
@JvmStatic
fun setIsMigratingKeyPair(context: Context?, newValue: Boolean) {
setBooleanPreference(context!!, "is_migrating_key_pair", newValue)
fun shouldUpdateProfile(context: Context, profileUpdateTime: Long): Boolean {
return profileUpdateTime > getLongPreference(context, LAST_PROFILE_UPDATE_TIME, 0)
}
@JvmStatic
@@ -760,18 +705,6 @@ object TextSecurePreferences {
setLongPreference(context, LAST_PROFILE_UPDATE_TIME, profileUpdateTime)
}
@JvmStatic
fun shouldUpdateProfile(context: Context, profileUpdateTime: Long) =
profileUpdateTime > getLongPreference(context, LAST_PROFILE_UPDATE_TIME, 0)
fun hasPerformedContactMigration(context: Context): Boolean {
return getBooleanPreference(context, "has_performed_contact_migration", false)
}
fun setPerformedContactMigration(context: Context) {
setBooleanPreference(context, "has_performed_contact_migration", true)
}
fun getLastOpenTimeDate(context: Context): Long {
return getLongPreference(context, LAST_OPEN_DATE, 0)
}
@@ -779,4 +712,17 @@ object TextSecurePreferences {
fun setLastOpenDate(context: Context) {
setLongPreference(context, LAST_OPEN_DATE, System.currentTimeMillis())
}
fun hasSeenLinkPreviewSuggestionDialog(context: Context): Boolean {
return getBooleanPreference(context, "has_seen_link_preview_suggestion_dialog", false)
}
fun setHasSeenLinkPreviewSuggestionDialog(context: Context) {
setBooleanPreference(context, "has_seen_link_preview_suggestion_dialog", true)
}
@JvmStatic
fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit()
}
}

View File

@@ -410,10 +410,19 @@ public class Recipient implements RecipientModifiedListener {
return address.isGroup();
}
public boolean isContactRecipient() {
return address.isContact();
}
public boolean isOpenGroupRecipient() {
return address.isOpenGroup();
}
public boolean isClosedGroupRecipient() {
return address.isClosedGroup();
}
@Deprecated
public boolean isPushGroupRecipient() {
return address.isGroup();
}
@@ -455,13 +464,6 @@ public class Recipient implements RecipientModifiedListener {
return (new TransparentContactPhoto()).asDrawable(context, getColor().toAvatarColor(context), inverted);
}
// public synchronized @NonNull FallbackContactPhoto getFallbackContactPhoto() {
// // TODO: I believe this is now completely unused
// if (isResolving()) return new TransparentContactPhoto();
// else if (isGroupRecipient()) return new GeneratedContactPhoto(name, R.drawable.ic_profile_default);
// else { return new TransparentContactPhoto(); }
// }
public synchronized @Nullable ContactPhoto getContactPhoto() {
if (isLocalNumber) return new ProfileContactPhoto(address, String.valueOf(TextSecurePreferences.getProfileAvatarId(context)));
else if (isGroupRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId);

View File

@@ -1,4 +0,0 @@
<vector android:height="260dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="260dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/gray20" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@@ -119,17 +119,7 @@
<item>vi</item>
</string-array>
<string-array name="pref_theme_entries">
<item>@string/preferences__light_theme</item>
<item>@string/preferences__dark_theme</item>
</string-array>
<string-array name="pref_theme_values" translatable="false">
<item>light</item>
<item>dark</item>
</string-array>
<string-array name="pref_led_color_entries">
<string-array name="pref_led_color_entries">
<item>@string/preferences__green</item>
<item>@string/preferences__red</item>
<item>@string/preferences__blue</item>
@@ -200,19 +190,7 @@
<item>@string/arrays__mute_for_one_year</item>
</string-array>
<string-array name="recipient_vibrate_entries">
<item>@string/arrays__settings_default</item>
<item>@string/arrays__enabled</item>
<item>@string/arrays__disabled</item>
</string-array>
<string-array name="recipient_vibrate_values">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<string-array name="pref_notification_privacy_entries">
<string-array name="pref_notification_privacy_entries">
<item>@string/arrays__name_and_message</item>
<item>@string/arrays__name_only</item>
<item>@string/arrays__no_name_or_message</item>
@@ -225,21 +203,8 @@
</string-array>
<!-- discrete MIME type (the part before the "/") -->
<string-array name="pref_media_download_entries">
<item>image</item>
<item>audio</item>
<item>video</item>
<item>documents</item>
</string-array>
<string-array name="pref_media_download_values">
<item>@string/arrays__images</item>
<item>@string/arrays__audio</item>
<item>@string/arrays__video</item>
<item>@string/arrays__documents</item>
</string-array>
<string-array name="pref_media_download_mobile_data_default">
<string-array name="pref_media_download_mobile_data_default">
<item>image</item>
<item>audio</item>
</string-array>
@@ -282,20 +247,6 @@
<item>#000000</item>
</array>
<string-array name="pref_message_font_size_entries">
<item>@string/arrays__small</item>
<item>@string/arrays__normal</item>
<item>@string/arrays__large</item>
<item>@string/arrays__extra_large</item>
</string-array>
<string-array name="pref_message_font_size_values">
<item>13</item>
<item>16</item>
<item>20</item>
<item>30</item>
</string-array>
<string-array name="pref_notification_priority_entries">
<item>@string/arrays__default</item>
<item>@string/arrays__high</item>

View File

@@ -1,26 +1,18 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<!-- Session -->
<color name="accent">#00F782</color>
<color name="accent_alpha50">#8000F782</color>
<color name="text">#FFFFFF</color>
<color name="destructive">#FF453A</color>
<color name="unimportant">#D8D8D8</color>
<color name="profile_picture_border">#23FFFFFF</color>
<color name="profile_picture_background">#353535</color>
<color name="border">#979797</color>
<color name="cell_background">#1B1B1B</color>
<color name="cell_selected">#0C0C0C</color>
<color name="action_bar_background">#171717</color>
<color name="navigation_bar_background">#121212</color>
<color name="separator">#36383C</color>
<color name="unimportant_button_background">#323232</color>
<color name="dialog_background">#101011</color>
<color name="dialog_border">#212121</color>
<color name="unimportant_dialog_button_background">@color/unimportant_button_background</color>
<color name="fake_chat_bubble_background">#3F4146</color>
<color name="fake_chat_bubble_text">#000000</color>
<color name="app_icon_background">#333132</color>
<color name="progress_bar_background">#0AFFFFFF</color>
<color name="compose_view_background">#1B1B1B</color>
@@ -38,25 +30,17 @@
<item>#f3c615</item>
<item>#fcac5a</item>
</array>
<!-- Session -->
<!-- Loki -->
<color name="loki_green">#78be20</color>
<color name="loki_dark_green">#419B41</color>
<color name="loki_darkest_gray">#0a0a0a</color>
<!-- Loki -->
<color name="signal_primary">@color/accent</color>
<color name="signal_primary_dark">@color/accent</color>
<color name="signal_primary_alpha_focus">#882090ea</color>
<color name="textsecure_primary">@color/signal_primary</color>
<color name="textsecure_primary_dark">@color/signal_primary_dark</color>
<color name="white">#ffffffff</color>
<color name="black">#ff000000</color>
<color name="gray5">#ffeeeeee</color>
<color name="gray10">#ffdddddd</color>
<color name="gray12">#ffe0e0e0</color>
<color name="gray13">#ffababab</color>
<color name="gray20">#ffcccccc</color>
@@ -65,28 +49,16 @@
<color name="gray65">#ff595959</color>
<color name="gray70">#ff4d4d4d</color>
<color name="gray78">#ff383838</color>
<color name="gray95">#ff111111</color>
<color name="transparent_black_05">#05000000</color>
<color name="transparent_black_10">#10000000</color>
<color name="transparent_black_20">#20000000</color>
<color name="transparent_black_30">#30000000</color>
<color name="transparent_black_40">#40000000</color>
<color name="transparent_black_70">#70000000</color>
<color name="transparent_black_90">#90000000</color>
<color name="transparent_white_05">#05ffffff</color>
<color name="transparent_white_10">#10ffffff</color>
<color name="transparent_white_20">#20ffffff</color>
<color name="transparent_white_30">#30ffffff</color>
<color name="transparent_white_40">#40ffffff</color>
<color name="transparent_white_60">#60ffffff</color>
<color name="transparent_white_70">#70ffffff</color>
<color name="transparent_white_aa">#aaffffff</color>
<color name="transparent_white_bb">#bbffffff</color>
<color name="transparent_white_dd">#ddffffff</color>
<color name="conversation_compose_divider">#32000000</color>
<color name="action_mode_status_bar">@color/gray65</color>
<color name="touch_highlight">#22000000</color>
@@ -94,27 +66,13 @@
<color name="device_link_item_background_light">#ffffffff</color>
<color name="device_link_item_background_dark">#ff333333</color>
<color name="import_export_item_background_light">#ffeeeeee</color>
<color name="import_export_item_background_dark">#ff333333</color>
<color name="import_export_item_background_shadow_light">#ffd5d5d5</color>
<color name="import_export_item_background_shadow_dark">#ff222222</color>
<color name="import_export_touch_highlight_light">#400099cc</color>
<color name="import_export_touch_highlight_dark">#40ffffff</color>
<color name="StickerPreviewActivity_remove_button_color">@color/conversation_crimson</color>
<color name="StickerPreviewActivity_install_button_color">@color/core_blue</color>
<color name="sticker_selected_color">#99ffffff</color>
<color name="transparent">#00FFFFFF</color>
<color name="transparent_black">#00000000</color>
<color name="MediaOverview_Media_selected_overlay">#88000000</color>
<color name="logsubmit_confirmation_background">#44ff2d00</color>
<color name="avatar_background">@color/transparent_black_90</color>
<color name="default_background_start">#121212</color>
<color name="default_background_end">#171717</color>
</resources>

View File

@@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="core_blue">#5bca5b</color>
<color name="core_green">#4caf50</color>
<color name="core_red">#f44336</color>
<color name="core_red_highlight">#ef5350</color>
<color name="core_white">#ffffff</color>
<color name="core_black">#000000</color>

View File

@@ -22,7 +22,6 @@
<dimen name="large_profile_picture_size">76dp</dimen>
<dimen name="conversation_view_status_indicator_size">14dp</dimen>
<dimen name="border_thickness">1dp</dimen>
<dimen name="profile_picture_border_thickness">1dp</dimen>
<dimen name="new_conversation_button_collapsed_size">60dp</dimen>
<dimen name="new_conversation_button_expanded_size">72dp</dimen>
<dimen name="tab_bar_height">36dp</dimen>
@@ -55,18 +54,10 @@
<dimen name="default_custom_keyboard_size">220dp</dimen>
<dimen name="min_custom_keyboard_size">110dp</dimen>
<dimen name="min_custom_keyboard_top_margin_portrait">170dp</dimen>
<dimen name="min_custom_keyboard_top_margin_landscape">50dp</dimen>
<dimen name="emoji_drawer_item_padding">5dp</dimen>
<dimen name="emoji_drawer_indicator_height">1.5dp</dimen>
<dimen name="emoji_drawer_left_right_padding">5dp</dimen>
<dimen name="emoji_drawer_item_width">48dp</dimen>
<dimen name="conversation_item_body_text_size">16sp</dimen>
<dimen name="conversation_item_date_text_size">12sp</dimen>
<dimen name="transport_selection_popup_width">200sp</dimen>
<dimen name="transport_selection_popup_xoff">2dp</dimen>
<dimen name="transport_selection_popup_yoff">2dp</dimen>
<dimen name="contact_photo_target_size">64dp</dimen>
<dimen name="contact_selection_photo_size">50dp</dimen>
<dimen name="album_total_width">210dp</dimen>
<dimen name="album_2_total_height">105dp</dimen>
@@ -80,14 +71,11 @@
<dimen name="album_5_cell_size_big">104dp</dimen>
<dimen name="album_5_cell_size_small">69dp</dimen>
<dimen name="message_corner_radius">10dp</dimen>
<dimen name="message_corner_radius">18dp</dimen>
<dimen name="message_corner_collapse_radius">4dp</dimen>
<dimen name="message_bubble_corner_radius">2dp</dimen>
<dimen name="message_bubble_shadow_distance">1.5dp</dimen>
<dimen name="message_bubble_horizontal_padding">@dimen/medium_spacing</dimen>
<dimen name="message_bubble_top_padding">@dimen/medium_spacing</dimen>
<dimen name="message_bubble_collapsed_footer_padding">@dimen/small_spacing</dimen>
<dimen name="message_bubble_edge_margin">32dp</dimen>
<dimen name="message_bubble_bottom_padding">@dimen/medium_spacing</dimen>
<dimen name="media_bubble_remove_button_size">24dp</dimen>
<dimen name="media_bubble_edit_button_size">24dp</dimen>
@@ -96,7 +84,6 @@
<dimen name="media_bubble_max_width">240dp</dimen>
<dimen name="media_bubble_min_height">100dp</dimen>
<dimen name="media_bubble_max_height">320dp</dimen>
<dimen name="media_bubble_sticker_dimens">128dp</dimen>
<dimen name="media_picker_folder_width">175dp</dimen>
<dimen name="media_picker_item_width">85dp</dimen>
@@ -109,39 +96,17 @@
<dimen name="thumbnail_default_radius">4dp</dimen>
<dimen name="conversation_compose_height">40dp</dimen>
<dimen name="conversation_individual_right_gutter">@dimen/large_spacing</dimen>
<dimen name="conversation_individual_left_gutter">@dimen/large_spacing</dimen>
<dimen name="conversation_group_left_gutter">60dp</dimen>
<dimen name="conversation_vertical_message_spacing_default">8dp</dimen>
<dimen name="conversation_vertical_message_spacing_collapse">1dp</dimen>
<dimen name="conversation_item_avatar_size">36dp</dimen>
<dimen name="quote_corner_radius_large">10dp</dimen>
<dimen name="quote_corner_radius_bottom">2dp</dimen>
<dimen name="quote_corner_radius_preview">16dp</dimen>
<integer name="media_overview_cols">3</integer>
<dimen name="message_details_table_row_pad">10dp</dimen>
<dimen name="quick_camera_shutter_ring_size">52dp</dimen>
<dimen name="sticker_page_item_padding">8dp</dimen>
<dimen name="sticker_page_item_divisor">88dp</dimen>
<dimen name="sticker_page_item_multiplier">8dp</dimen>
<dimen name="sticker_preview_sticker_size">96dp</dimen>
<dimen name="sticker_management_horizontal_margin">16dp</dimen>
<dimen name="tooltip_popup_margin">8dp</dimen>
<dimen name="transfer_controls_expanded_width">150dp</dimen>
<dimen name="transfer_controls_contracted_width">70dp</dimen>
<dimen name="conversation_list_fragment_archive_padding">16dp</dimen>
<dimen name="contact_selection_actions_tap_area">10dp</dimen>
<dimen name="unread_count_bubble_radius">13sp</dimen>
<dimen name="unread_count_bubble_diameter">26sp</dimen>
<!-- RedPhone -->
@@ -154,18 +119,10 @@
holding the phone, *before* moving it up to your face and having
the prox sensor kick in.) -->
<dimen name="onboarding_title_size">34sp</dimen>
<dimen name="onboarding_subtitle_size">20sp</dimen>
<dimen name="scribble_stroke_size">2dp</dimen>
<dimen name="floating_action_button_margin">16dp</dimen>
<dimen name="alertview_small_icon_size">14dp</dimen>
<dimen name="recording_voice_lock_target">-96dp</dimen>
<dimen name="default_margin">16dp</dimen>
<dimen name="drawable_padding">24dp</dimen>
<dimen name="text_size">16sp</dimen>
<dimen name="normal_padding">16dp</dimen>

View File

@@ -15,22 +15,6 @@
<resources>
<string name="PlaystoreDescription_para_0100">TextSecure Private Messenger</string>
<string name="PlaystoreDescription_para_0200">TextSecure is a messaging app that allows you to take back your privacy while easily communicating with friends.</string>
<string name="PlaystoreDescription_para_0300">Using TextSecure, you can communicate instantly while avoiding SMS fees, create groups so that you can chat in real time with all your friends at once, and share media or attachments all with complete privacy. The server never has access to any of your communication and never stores any of your data.</string>
<string name="PlaystoreDescription_para_0400">* Private. TextSecure uses an advanced end to end encryption protocol that provides privacy for every message every time.</string>
<string name="PlaystoreDescription_para_0500">* Open Source. TextSecure is Free and Open Source, enabling anyone to verify its security by auditing the code. TextSecure is the only private messenger that uses open source peer-reviewed cryptographic protocols to keep your messages safe.</string>
<string name="PlaystoreDescription_para_0600">* Group Chat. TextSecure allows you to create encrypted groups so you can have private conversations with all your friends at once. Not only are the messages encrypted, but the TextSecure server never has access to any group metadata such as the membership list, group title, or group icon.</string>
<string name="PlaystoreDescription_para_0700">* Fast. The TextSecure protocol is designed to operate in the most constrained environment possible. Using TextSecure, messages are instantly delivered to friends.</string>
<string name="PlaystoreDescription_para_0800">Please file any bugs, issues, or feature requests at:</string>
<string name="PlaystoreDescription_para_0900">https://github.com/signalapp/textsecure/issues</string>
<string name="PlaystoreDescription_para_1000">More details:</string>
<string name="PlaystoreDescription_para_1100">http://www.whispersystems.org/#encrypted_texts</string>
<!-- EOF -->
<!-- EOF -->
</resources>

View File

@@ -1,278 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="red_50">#FFEBEE</color>
<color name="red_100">#FFCDD2</color>
<color name="red_200">#EF9A9A</color>
<color name="red_300">#E57373</color>
<color name="red_400">#EF5350</color>
<color name="red_500">#F44336</color>
<color name="red_600">#E53935</color>
<color name="red_700">#D32F2F</color>
<color name="red_800">#C62828</color>
<color name="red_900">#B71C1C</color>
<color name="red_A100">#FF8A80</color>
<color name="red_A200">#FF5252</color>
<color name="red_A400">#FF1744</color>
<color name="red_A700">#D50000</color>
<color name="deep_purple_50">#EDE7F6</color>
<color name="deep_purple_100">#D1C4E9</color>
<color name="deep_purple_200">#B39DDB</color>
<color name="deep_purple_300">#9575CD</color>
<color name="deep_purple_400">#7E57C2</color>
<color name="deep_purple_500">#673AB7</color>
<color name="deep_purple_600">#5E35B1</color>
<color name="deep_purple_700">#512DA8</color>
<color name="deep_purple_800">#4527A0</color>
<color name="deep_purple_900">#311B92</color>
<color name="deep_purple_A100">#B388FF</color>
<color name="deep_purple_A200">#7C4DFF</color>
<color name="deep_purple_A400">#651FFF</color>
<color name="deep_purple_A700">#6200EA</color>
<color name="light_blue_50">#E1F5FE</color>
<color name="light_blue_100">#B3E5FC</color>
<color name="light_blue_200">#81D4FA</color>
<color name="light_blue_300">#4FC3F7</color>
<color name="light_blue_400">#29B6F6</color>
<color name="light_blue_500">#03A9F4</color>
<color name="light_blue_600">#039BE5</color>
<color name="light_blue_700">#0288D1</color>
<color name="light_blue_800">#0277BD</color>
<color name="light_blue_900">#01579B</color>
<color name="light_blue_A100">#80D8FF</color>
<color name="light_blue_A200">#40C4FF</color>
<color name="light_blue_A400">#00B0FF</color>
<color name="light_blue_A700">#0091EA</color>
<color name="green_50">#E8F5E9</color>
<color name="green_100">#C8E6C9</color>
<color name="green_200">#A5D6A7</color>
<color name="green_300">#81C784</color>
<color name="green_400">#66BB6A</color>
<color name="green_500">#4CAF50</color>
<color name="green_600">#43A047</color>
<color name="green_700">#388E3C</color>
<color name="green_800">#2E7D32</color>
<color name="green_900">#1B5E20</color>
<color name="green_A100">#B9F6CA</color>
<color name="green_A200">#69F0AE</color>
<color name="green_A400">#00E676</color>
<color name="green_A700">#00C853</color>
<color name="yellow_50">#FFFDE7</color>
<color name="yellow_100">#FFF9C4</color>
<color name="yellow_200">#FFF59D</color>
<color name="yellow_300">#FFF176</color>
<color name="yellow_400">#FFEE58</color>
<color name="yellow_500">#FFEB3B</color>
<color name="yellow_600">#FDD835</color>
<color name="yellow_700">#FBC02D</color>
<color name="yellow_800">#F9A825</color>
<color name="yellow_900">#F57F17</color>
<color name="yellow_A100">#FFFF8D</color>
<color name="yellow_A200">#FFFF00</color>
<color name="yellow_A400">#FFEA00</color>
<color name="yellow_A700">#FFD600</color>
<color name="deep_orange_50">#FBE9E7</color>
<color name="deep_orange_100">#FFCCBC</color>
<color name="deep_orange_200">#FFAB91</color>
<color name="deep_orange_300">#FF8A65</color>
<color name="deep_orange_400">#FF7043</color>
<color name="deep_orange_500">#FF5722</color>
<color name="deep_orange_600">#F4511E</color>
<color name="deep_orange_700">#E64A19</color>
<color name="deep_orange_800">#D84315</color>
<color name="deep_orange_900">#BF360C</color>
<color name="deep_orange_A100">#FF9E80</color>
<color name="deep_orange_A200">#FF6E40</color>
<color name="deep_orange_A400">#FF3D00</color>
<color name="deep_orange_A700">#DD2C00</color>
<color name="blue_grey_50">#ECEFF1</color>
<color name="blue_grey_100">#CFD8DC</color>
<color name="blue_grey_200">#B0BEC5</color>
<color name="blue_grey_300">#90A4AE</color>
<color name="blue_grey_400">#78909C</color>
<color name="blue_grey_500">#607D8B</color>
<color name="blue_grey_600">#546E7A</color>
<color name="blue_grey_700">#455A64</color>
<color name="blue_grey_800">#37474F</color>
<color name="blue_grey_900">#263238</color>
<color name="pink_50">#FCE4EC</color>
<color name="pink_100">#F8BBD0</color>
<color name="pink_200">#F48FB1</color>
<color name="pink_300">#F06292</color>
<color name="pink_400">#EC407A</color>
<color name="pink_500">#E91E63</color>
<color name="pink_600">#D81B60</color>
<color name="pink_700">#C2185B</color>
<color name="pink_800">#AD1457</color>
<color name="pink_900">#880E4F</color>
<color name="pink_A100">#FF80AB</color>
<color name="pink_A200">#FF4081</color>
<color name="pink_A400">#F50057</color>
<color name="pink_A700">#C51162</color>
<color name="indigo_50">#E8EAF6</color>
<color name="indigo_100">#C5CAE9</color>
<color name="indigo_200">#9FA8DA</color>
<color name="indigo_300">#7986CB</color>
<color name="indigo_400">#5C6BC0</color>
<color name="indigo_500">#3F51B5</color>
<color name="indigo_600">#3949AB</color>
<color name="indigo_700">#303F9F</color>
<color name="indigo_800">#283593</color>
<color name="indigo_900">#1A237E</color>
<color name="indigo_A100">#8C9EFF</color>
<color name="indigo_A200">#536DFE</color>
<color name="indigo_A400">#3D5AFE</color>
<color name="indigo_A700">#304FFE</color>
<color name="cyan_50">#E0F7FA</color>
<color name="cyan_100">#B2EBF2</color>
<color name="cyan_200">#80DEEA</color>
<color name="cyan_300">#4DD0E1</color>
<color name="cyan_400">#26C6DA</color>
<color name="cyan_500">#00BCD4</color>
<color name="cyan_600">#00ACC1</color>
<color name="cyan_700">#0097A7</color>
<color name="cyan_800">#00838F</color>
<color name="cyan_900">#006064</color>
<color name="cyan_A100">#84FFFF</color>
<color name="cyan_A200">#18FFFF</color>
<color name="cyan_A400">#00E5FF</color>
<color name="cyan_A700">#00B8D4</color>
<color name="light_green_50">#F1F8E9</color>
<color name="light_green_100">#DCEDC8</color>
<color name="light_green_200">#C5E1A5</color>
<color name="light_green_300">#AED581</color>
<color name="light_green_400">#9CCC65</color>
<color name="light_green_500">#8BC34A</color>
<color name="light_green_600">#7CB342</color>
<color name="light_green_700">#689F38</color>
<color name="light_green_800">#558B2F</color>
<color name="light_green_900">#33691E</color>
<color name="light_green_A100">#CCFF90</color>
<color name="light_green_A200">#B2FF59</color>
<color name="light_green_A400">#76FF03</color>
<color name="light_green_A700">#64DD17</color>
<color name="amber_50">#FFF8E1</color>
<color name="amber_100">#FFECB3</color>
<color name="amber_200">#FFE082</color>
<color name="amber_300">#FFD54F</color>
<color name="amber_400">#FFCA28</color>
<color name="amber_500">#FFC107</color>
<color name="amber_600">#FFB300</color>
<color name="amber_700">#FFA000</color>
<color name="amber_800">#FF8F00</color>
<color name="amber_900">#FF6F00</color>
<color name="amber_A100">#FFE57F</color>
<color name="amber_A200">#FFD740</color>
<color name="amber_A400">#FFC400</color>
<color name="amber_A700">#FFAB00</color>
<color name="brown_50">#EFEBE9</color>
<color name="brown_100">#D7CCC8</color>
<color name="brown_200">#BCAAA4</color>
<color name="brown_300">#A1887F</color>
<color name="brown_400">#8D6E63</color>
<color name="brown_500">#795548</color>
<color name="brown_600">#6D4C41</color>
<color name="brown_700">#5D4037</color>
<color name="brown_800">#4E342E</color>
<color name="brown_900">#3E2723</color>
<color name="purple_50">#F3E5F5</color>
<color name="purple_100">#E1BEE7</color>
<color name="purple_200">#CE93D8</color>
<color name="purple_300">#BA68C8</color>
<color name="purple_400">#AB47BC</color>
<color name="purple_500">#9C27B0</color>
<color name="purple_600">#8E24AA</color>
<color name="purple_700">#7B1FA2</color>
<color name="purple_800">#6A1B9A</color>
<color name="purple_900">#4A148C</color>
<color name="purple_A100">#EA80FC</color>
<color name="purple_A200">#E040FB</color>
<color name="purple_A400">#D500F9</color>
<color name="purple_A700">#AA00FF</color>
<color name="blue_50">#E3F2FD</color>
<color name="blue_100">#BBDEFB</color>
<color name="blue_200">#90CAF9</color>
<color name="blue_300">#64B5F6</color>
<color name="blue_400">#42A5F5</color>
<color name="blue_500">#2196F3</color>
<color name="blue_600">#1E88E5</color>
<color name="blue_700">#1976D2</color>
<color name="blue_800">#1565C0</color>
<color name="blue_900">#0D47A1</color>
<color name="blue_A100">#82B1FF</color>
<color name="blue_A200">#448AFF</color>
<color name="blue_A400">#2979FF</color>
<color name="blue_A700">#2962FF</color>
<color name="teal_50">#E0F2F1</color>
<color name="teal_100">#B2DFDB</color>
<color name="teal_200">#80CBC4</color>
<color name="teal_300">#4DB6AC</color>
<color name="teal_400">#26A69A</color>
<color name="teal_500">#009688</color>
<color name="teal_600">#00897B</color>
<color name="teal_700">#00796B</color>
<color name="teal_800">#00695C</color>
<color name="teal_900">#004D40</color>
<color name="teal_A100">#A7FFEB</color>
<color name="teal_A200">#64FFDA</color>
<color name="teal_A400">#1DE9B6</color>
<color name="teal_A700">#00BFA5</color>
<color name="lime_50">#F9FBE7</color>
<color name="lime_100">#F0F4C3</color>
<color name="lime_200">#E6EE9C</color>
<color name="lime_300">#DCE775</color>
<color name="lime_400">#D4E157</color>
<color name="lime_500">#CDDC39</color>
<color name="lime_600">#C0CA33</color>
<color name="lime_700">#AFB42B</color>
<color name="lime_800">#9E9D24</color>
<color name="lime_900">#827717</color>
<color name="lime_A100">#F4FF81</color>
<color name="lime_A200">#EEFF41</color>
<color name="lime_A400">#C6FF00</color>
<color name="lime_A700">#AEEA00</color>
<color name="orange_50">#FFF3E0</color>
<color name="orange_100">#FFE0B2</color>
<color name="orange_200">#FFCC80</color>
<color name="orange_300">#FFB74D</color>
<color name="orange_400">#FFA726</color>
<color name="orange_500">#FF9800</color>
<color name="orange_600">#FB8C00</color>
<color name="orange_700">#F57C00</color>
<color name="orange_800">#EF6C00</color>
<color name="orange_900">#E65100</color>
<color name="orange_A100">#FFD180</color>
<color name="orange_A200">#FFAB40</color>
<color name="orange_A400">#FF9100</color>
<color name="orange_A700">#FF6D00</color>
<color name="grey_50">#FAFAFA</color>
<color name="grey_100">#F5F5F5</color>
<color name="grey_200">#EEEEEE</color>
<color name="grey_300">#E0E0E0</color>
<color name="grey_400">#BDBDBD</color>
<color name="grey_500">#9E9E9E</color>
<color name="grey_600">#757575</color>
<color name="grey_700">#616161</color>
<color name="grey_800">#424242</color>
<color name="grey_900">#212121</color>
<color name="grey_400_transparent">#44BDBDBD</color>
</resources>

View File

@@ -1,15 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Signal.Text.Headline" parent="Base.TextAppearance.AppCompat.Headline">
<item name="android:textSize">28sp</item>
<item name="android:lineSpacingExtra">3sp</item>
<item name="android:fontFamily">sans-serif</item>
<item name="android:letterSpacing" tools:ignore="NewApi">0</item>
</style>
<style name="Signal.Text.Headline.Registration" parent="Signal.Text.Headline">
<item name="android:fontFamily">sans-serif-medium</item>
</style>
<style name="Signal.Text.Body" parent="Base.TextAppearance.AppCompat.Body1">
<item name="android:textSize">16sp</item>
@@ -38,14 +28,4 @@
<item name="android:shadowRadius">0</item>
</style>
<style name="Signal.Text.Caption.MessageReceived">
<item name="android:textColor">?conversation_item_received_text_secondary_color</item>
<item name="android:shadowRadius">0</item>
<item name="android:alpha">0.7</item>
</style>
<style name="Signal.Text.Caption.MessageImageOverlay">
<item name="android:textColor">@color/core_white</item>
</style>
</resources>