mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 19:07:40 +00:00
Merge branch 'dev' into security
This commit is contained in:
@@ -6,6 +6,10 @@ plugins {
|
||||
android {
|
||||
compileSdkVersion androidCompileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion androidMinimumSdkVersion
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
|
@@ -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?
|
||||
}
|
@@ -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>)
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
}
|
@@ -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() {
|
||||
|
@@ -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()
|
||||
|
@@ -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) {
|
||||
|
@@ -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) {
|
||||
|
@@ -34,7 +34,7 @@ class DataExtractionNotification() : ControlMessage() {
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(kind: Kind) : this() {
|
||||
constructor(kind: Kind) : this() {
|
||||
this.kind = kind
|
||||
}
|
||||
|
||||
|
@@ -33,7 +33,7 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(duration: Int) : this() {
|
||||
constructor(duration: Int) : this() {
|
||||
this.syncTarget = null
|
||||
this.duration = duration
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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>) {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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,
|
||||
/**
|
||||
|
@@ -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.
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
@@ -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>
|
Reference in New Issue
Block a user