Merge branch 'dev' into security

This commit is contained in:
Niels Andriesse
2021-07-08 10:50:25 +10:00
434 changed files with 12040 additions and 13916 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

@@ -20,6 +20,7 @@ interface MessageDataProvider {
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream)
fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long, threadId: Long)
fun isOutgoingMessage(timestamp: Long): Boolean
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
fun handleFailedAttachmentUpload(attachmentId: Long)

View File

@@ -89,6 +89,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)
@@ -115,6 +116,8 @@ interface StorageProtocol {
fun isClosedGroup(publicKey: String): Boolean
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
fun updateFormationTimestamp(groupID: String, formationTimestamp: Long)
fun setExpirationTimer(groupID: String, duration: Int)
// Groups
fun getAllGroups(): List<GroupRecord>

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

@@ -4,13 +4,18 @@ 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.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
class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) : Job {
override var delegate: JobDelegate? = null
@@ -23,7 +28,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
}
// Settings
override val maxFailureCount: Int = 20
override val maxFailureCount: Int = 100
companion object {
val KEY: String = "AttachmentDownloadJob"
@@ -37,46 +42,68 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
val storage = MessagingModuleConfiguration.shared.storage
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
if (exception == Error.NoAttachment) {
if (exception == Error.NoAttachment
|| (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 400)) {
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
this.handlePermanentFailure(exception)
} else {
this.handleFailure(exception)
}
}
var tempFile: File? = null
try {
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
?: return handleFailure(Error.NoAttachment)
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
val tempFile = createTempFile()
tempFile = createTempFile()
val threadID = storage.getThreadIdForMms(databaseMessageID)
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) {
tempFile?.delete()
return handleFailure(e)
}
}
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

@@ -23,6 +23,7 @@ class JobQueue : JobDelegate {
private val attachmentDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
private val scope = GlobalScope + SupervisorJob()
private val queue = Channel<Job>(UNLIMITED)
private val pendingJobIds = mutableSetOf<String>()
val timer = Timer()
@@ -86,6 +87,19 @@ 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() {
if (hasResumedPendingJobs) {
Log.d("Loki", "resumePendingJobs() should only be called once.")
@@ -120,6 +134,7 @@ class JobQueue : JobDelegate {
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 +184,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,6 +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.expirationTimer >= 0
}
is Kind.EncryptionKeyPair -> true
is Kind.NameChange -> kind.name.isNotEmpty()
@@ -44,8 +45,8 @@ 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>) : Kind() {
internal constructor() : this(ByteString.EMPTY, "", null, listOf(), listOf())
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.
*
@@ -88,10 +89,11 @@ class ClosedGroupControlMessage() : ControlMessage() {
val publicKey = closedGroupControlMessageProto.publicKey ?: return null
val name = closedGroupControlMessageProto.name ?: return null
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
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)
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
@@ -140,9 +142,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
closedGroupControlMessage.addAllMembers(kind.members)
closedGroupControlMessage.addAllAdmins(kind.admins)
closedGroupControlMessage.expirationTimer = kind.expirationTimer
}
is Kind.EncryptionKeyPair -> {
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
@@ -170,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

@@ -11,8 +11,10 @@ import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.Curve
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.utilities.guava.Optional
@@ -50,7 +52,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis())
storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Send a closed group update message to all members individually
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData, 0)
val sentTime = System.currentTimeMillis()
for (member in members) {
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
@@ -131,6 +133,8 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
Log.d("Loki", "Can't add members to nonexistent closed group.")
throw Error.NoThread
}
val recipient = Recipient.from(context, fromSerialized(groupID), false)
val expireTimer = recipient.expireMessages
if (membersToAdd.isEmpty()) {
Log.d("Loki", "Invalid closed group update.")
throw Error.InvalidClosedGroupUpdate
@@ -155,9 +159,15 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
send(closedGroupControlMessage, Address.fromSerialized(groupID))
// Send closed group update messages to any new members individually
for (member in membersToAdd) {
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData, expireTimer)
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
closedGroupControlMessage.sentTimestamp = sentTime
// It's important that the sent timestamp of this message is greater than the sent timestamp
// of the `MembersAdded` message above. The reason is that upon receiving this `New` message,
// the recipient will update the closed group formation timestamp and ignore any closed group
// updates from before that timestamp. By setting the timestamp of the message below to a value
// greater than that of the `MembersAdded` message, we ensure that newly added members ignore
// the `MembersAdded` message.
closedGroupControlMessage.sentTimestamp = System.currentTimeMillis()
send(closedGroupControlMessage, Address.fromSerialized(member))
}
// Notify the user

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!!)
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!, closedGroup.expirationTimer)
}
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
for (openGroup in message.openGroups) {
@@ -256,41 +256,46 @@ 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() }
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!)
val expireTimer = kind.expirationTimer
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expireTimer)
}
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long, expireTimer: Int) {
val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
// Create the group
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
if (storage.getGroup(groupID) != null) {
// Update the group
// Clear zombie list if the group wasn't active
if (!storage.isGroupActive(groupPublicKey)) {
// Clear zombie list if the group wasn't active
storage.setZombieMembers(groupID, listOf())
// Update the formation timestamp
storage.updateFormationTimestamp(groupID, formationTimestamp)
}
storage.updateTitle(groupID, name)
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
} else {
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
// Notify the user
if (userPublicKey == sender) {
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
} else {
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
}
}
storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Add the group to the user's set of public keys to poll for
storage.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Set expiration timer
storage.setExpirationTimer(groupID, expireTimer)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!)
// Notify the user
if (userPublicKey == sender) {
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
} else {
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
}
// Start polling
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
}
@@ -424,7 +429,7 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) {
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else {
for (user in updateMembers) {
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
@@ -556,12 +561,12 @@ private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPu
val oldMembers = group.members.map { it.serialize() }
// Check that the message isn't from before the group was created
if (group.formationTimestamp > sentTimestamp) {
android.util.Log.d("Loki", "Ignoring closed group update from before thread was created.")
Log.d("Loki", "Ignoring closed group update from before thread was created.")
return false
}
// Check that the sender is a member of the group (before the update)
if (senderPublicKey !in oldMembers) {
android.util.Log.d("Loki", "Ignoring closed group info message from non-member.")
Log.d("Loki", "Ignoring closed group info message from non-member.")
return false
}
return true

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

@@ -433,11 +433,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

@@ -168,7 +168,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())) {
@@ -290,9 +290,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()
}
}

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

@@ -779,4 +779,12 @@ 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)
}
}

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>