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
|
||||
|
@@ -20,6 +20,7 @@ interface MessageDataProvider {
|
||||
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
|
||||
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
|
||||
fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream)
|
||||
fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long, threadId: Long)
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
|
||||
fun handleFailedAttachmentUpload(attachmentId: Long)
|
||||
|
@@ -89,6 +89,7 @@ interface StorageProtocol {
|
||||
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
|
||||
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
|
||||
fun markAsSending(timestamp: Long, author: String)
|
||||
fun markAsSent(timestamp: Long, author: String)
|
||||
fun markUnidentified(timestamp: Long, author: String)
|
||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||
@@ -115,6 +116,8 @@ interface StorageProtocol {
|
||||
fun isClosedGroup(publicKey: String): Boolean
|
||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
fun updateFormationTimestamp(groupID: String, formationTimestamp: Long)
|
||||
fun setExpirationTimer(groupID: String, duration: Int)
|
||||
|
||||
// Groups
|
||||
fun getAllGroups(): List<GroupRecord>
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,13 +4,18 @@ import okhttp3.HttpUrl
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.utilities.Data
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.utilities.DecodedAudio
|
||||
import org.session.libsession.utilities.DownloadUtilities
|
||||
import org.session.libsession.utilities.InputStreamMediaDataSource
|
||||
import org.session.libsignal.streams.AttachmentCipherInputStream
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) : Job {
|
||||
override var delegate: JobDelegate? = null
|
||||
@@ -23,7 +28,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
}
|
||||
|
||||
// Settings
|
||||
override val maxFailureCount: Int = 20
|
||||
override val maxFailureCount: Int = 100
|
||||
|
||||
companion object {
|
||||
val KEY: String = "AttachmentDownloadJob"
|
||||
@@ -37,46 +42,68 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
|
||||
if (exception == Error.NoAttachment) {
|
||||
if (exception == Error.NoAttachment
|
||||
|| (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 400)) {
|
||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||
this.handlePermanentFailure(exception)
|
||||
} else {
|
||||
this.handleFailure(exception)
|
||||
}
|
||||
}
|
||||
var tempFile: File? = null
|
||||
try {
|
||||
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
||||
?: return handleFailure(Error.NoAttachment)
|
||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||
val tempFile = createTempFile()
|
||||
tempFile = createTempFile()
|
||||
val threadID = storage.getThreadIdForMms(databaseMessageID)
|
||||
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
||||
val inputStream = if (openGroupV2 == null) {
|
||||
if (openGroupV2 == null) {
|
||||
DownloadUtilities.downloadFile(tempFile, attachment.url)
|
||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
|
||||
FileInputStream(tempFile)
|
||||
} else {
|
||||
AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
||||
}
|
||||
} else {
|
||||
val url = HttpUrl.parse(attachment.url)!!
|
||||
val fileID = url.pathSegments().last()
|
||||
OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
||||
tempFile.writeBytes(it)
|
||||
}
|
||||
FileInputStream(tempFile)
|
||||
}
|
||||
val inputStream = getInputStream(tempFile, attachment)
|
||||
|
||||
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, inputStream)
|
||||
if (attachment.contentType.startsWith("audio/")) {
|
||||
// process the duration
|
||||
try {
|
||||
InputStreamMediaDataSource(getInputStream(tempFile, attachment)).use { mediaDataSource ->
|
||||
val durationMs = (DecodedAudio.create(mediaDataSource).totalDuration / 1000.0).toLong()
|
||||
messageDataProvider.updateAudioAttachmentDuration(
|
||||
attachment.attachmentId,
|
||||
durationMs,
|
||||
threadID
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't process audio attachment", e)
|
||||
}
|
||||
}
|
||||
tempFile.delete()
|
||||
handleSuccess()
|
||||
} catch (e: Exception) {
|
||||
tempFile?.delete()
|
||||
return handleFailure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInputStream(tempFile: File, attachment: DatabaseAttachment): InputStream {
|
||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||
return if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
|
||||
FileInputStream(tempFile)
|
||||
} else {
|
||||
AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccess() {
|
||||
Log.w(AttachmentUploadJob.TAG, "Attachment downloaded successfully.")
|
||||
Log.w("AttachmentDownloadJob", "Attachment downloaded successfully.")
|
||||
delegate?.handleJobSucceeded(this)
|
||||
}
|
||||
|
||||
|
@@ -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)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ class JobQueue : JobDelegate {
|
||||
private val attachmentDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
|
||||
private val scope = GlobalScope + SupervisorJob()
|
||||
private val queue = Channel<Job>(UNLIMITED)
|
||||
private val pendingJobIds = mutableSetOf<String>()
|
||||
|
||||
val timer = Timer()
|
||||
|
||||
@@ -86,6 +87,19 @@ class JobQueue : JobDelegate {
|
||||
MessagingModuleConfiguration.shared.storage.persistJob(job)
|
||||
}
|
||||
|
||||
fun resumePendingSendMessage(job: Job) {
|
||||
val id = job.id ?: run {
|
||||
Log.e("Loki", "tried to resume pending send job with no ID")
|
||||
return
|
||||
}
|
||||
if (!pendingJobIds.add(id)) {
|
||||
Log.e("Loki","tried to re-queue pending/in-progress job")
|
||||
return
|
||||
}
|
||||
queue.offer(job)
|
||||
Log.d("Loki", "resumed pending send message $id")
|
||||
}
|
||||
|
||||
fun resumePendingJobs() {
|
||||
if (hasResumedPendingJobs) {
|
||||
Log.d("Loki", "resumePendingJobs() should only be called once.")
|
||||
@@ -120,6 +134,7 @@ class JobQueue : JobDelegate {
|
||||
override fun handleJobSucceeded(job: Job) {
|
||||
val jobId = job.id ?: return
|
||||
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId)
|
||||
pendingJobIds.remove(jobId)
|
||||
}
|
||||
|
||||
override fun handleJobFailed(job: Job, error: Exception) {
|
||||
@@ -169,4 +184,7 @@ class JobQueue : JobDelegate {
|
||||
val maxBackoff = (10 * 60).toDouble() // 10 minutes
|
||||
return (1000 * 0.25 * min(maxBackoff, (2.0).pow(job.failureCount))).roundToLong()
|
||||
}
|
||||
|
||||
private fun Job.isSend() = this is MessageSendJob || this is AttachmentUploadJob
|
||||
|
||||
}
|
@@ -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,6 +34,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
is Kind.New -> {
|
||||
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair?.publicKey != null
|
||||
&& kind.encryptionKeyPair?.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
|
||||
&& kind.expirationTimer >= 0
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> true
|
||||
is Kind.NameChange -> kind.name.isNotEmpty()
|
||||
@@ -44,8 +45,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
}
|
||||
|
||||
sealed class Kind {
|
||||
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() {
|
||||
internal constructor() : this(ByteString.EMPTY, "", null, listOf(), listOf())
|
||||
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>, var expirationTimer: Int) : Kind() {
|
||||
internal constructor() : this(ByteString.EMPTY, "", null, listOf(), listOf(), 0)
|
||||
}
|
||||
/** An encryption key pair encrypted for each member individually.
|
||||
*
|
||||
@@ -88,10 +89,11 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val publicKey = closedGroupControlMessageProto.publicKey ?: return null
|
||||
val name = closedGroupControlMessageProto.name ?: return null
|
||||
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
|
||||
val expirationTimer = closedGroupControlMessageProto.expirationTimer
|
||||
try {
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()),
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList)
|
||||
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList, expirationTimer)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't parse key pair from proto: $encryptionKeyPairAsProto.")
|
||||
return null
|
||||
@@ -140,9 +142,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
closedGroupControlMessage.addAllAdmins(kind.admins)
|
||||
closedGroupControlMessage.expirationTimer = kind.expirationTimer
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
|
||||
@@ -170,10 +173,6 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
|
||||
// Group context
|
||||
setGroupContext(dataMessageProto)
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// if it receives a message without the current expiration timer value attached to it...
|
||||
dataMessageProto.expireTimer = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -11,8 +11,10 @@ import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.crypto.ecc.Curve
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
@@ -50,7 +52,7 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis())
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Send a closed group update message to all members individually
|
||||
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData, 0)
|
||||
val sentTime = System.currentTimeMillis()
|
||||
for (member in members) {
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
@@ -131,6 +133,8 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
|
||||
Log.d("Loki", "Can't add members to nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val recipient = Recipient.from(context, fromSerialized(groupID), false)
|
||||
val expireTimer = recipient.expireMessages
|
||||
if (membersToAdd.isEmpty()) {
|
||||
Log.d("Loki", "Invalid closed group update.")
|
||||
throw Error.InvalidClosedGroupUpdate
|
||||
@@ -155,9 +159,15 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData, expireTimer)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
// It's important that the sent timestamp of this message is greater than the sent timestamp
|
||||
// of the `MembersAdded` message above. The reason is that upon receiving this `New` message,
|
||||
// the recipient will update the closed group formation timestamp and ignore any closed group
|
||||
// updates from before that timestamp. By setting the timestamp of the message below to a value
|
||||
// greater than that of the `MembersAdded` message, we ensure that newly added members ignore
|
||||
// the `MembersAdded` message.
|
||||
closedGroupControlMessage.sentTimestamp = System.currentTimeMillis()
|
||||
send(closedGroupControlMessage, Address.fromSerialized(member))
|
||||
}
|
||||
// Notify the user
|
||||
|
@@ -121,7 +121,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
for (closedGroup in message.closedGroups) {
|
||||
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
|
||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
|
||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!, closedGroup.expirationTimer)
|
||||
}
|
||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
||||
for (openGroup in message.openGroups) {
|
||||
@@ -256,41 +256,46 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
|
||||
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
|
||||
val members = kind.members.map { it.toByteArray().toHexString() }
|
||||
val admins = kind.admins.map { it.toByteArray().toHexString() }
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!)
|
||||
val expireTimer = kind.expirationTimer
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expireTimer)
|
||||
}
|
||||
|
||||
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
|
||||
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long, expireTimer: Int) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
// Create the group
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
if (storage.getGroup(groupID) != null) {
|
||||
// Update the group
|
||||
// Clear zombie list if the group wasn't active
|
||||
if (!storage.isGroupActive(groupPublicKey)) {
|
||||
// Clear zombie list if the group wasn't active
|
||||
storage.setZombieMembers(groupID, listOf())
|
||||
// Update the formation timestamp
|
||||
storage.updateFormationTimestamp(groupID, formationTimestamp)
|
||||
}
|
||||
storage.updateTitle(groupID, name)
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
// Notify the user
|
||||
if (userPublicKey == sender) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Set expiration timer
|
||||
storage.setExpirationTimer(groupID, expireTimer)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!)
|
||||
// Notify the user
|
||||
if (userPublicKey == sender) {
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
|
||||
}
|
||||
// Start polling
|
||||
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||
}
|
||||
@@ -424,7 +429,7 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
||||
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
|
||||
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
for (user in updateMembers) {
|
||||
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
|
||||
@@ -556,12 +561,12 @@ private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPu
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check that the message isn't from before the group was created
|
||||
if (group.formationTimestamp > sentTimestamp) {
|
||||
android.util.Log.d("Loki", "Ignoring closed group update from before thread was created.")
|
||||
Log.d("Loki", "Ignoring closed group update from before thread was created.")
|
||||
return false
|
||||
}
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
if (senderPublicKey !in oldMembers) {
|
||||
android.util.Log.d("Loki", "Ignoring closed group info message from non-member.")
|
||||
Log.d("Loki", "Ignoring closed group info message from non-member.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@@ -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>) {
|
||||
|
@@ -433,11 +433,12 @@ object OnionRequestAPI {
|
||||
internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String? = null): Promise<Map<*, *>, Exception> {
|
||||
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
|
||||
return sendOnionRequest(Destination.Snode(snode), payload).recover { exception ->
|
||||
val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException
|
||||
if (httpRequestFailedException != null) {
|
||||
val error = SnodeAPI.handleSnodeError(httpRequestFailedException.statusCode, httpRequestFailedException.json, snode, publicKey)
|
||||
if (error != null) { throw error }
|
||||
val error = when (exception) {
|
||||
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
||||
is HTTPRequestFailedAtDestinationException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
|
||||
else -> null
|
||||
}
|
||||
if (error != null) { throw error }
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
@@ -168,7 +168,7 @@ object SnodeAPI {
|
||||
val validationCount = 3
|
||||
val sessionIDByteCount = 33
|
||||
// Hash the ONS name using BLAKE2b
|
||||
val onsName = onsName.toLowerCase(Locale.ENGLISH)
|
||||
val onsName = onsName.toLowerCase(Locale.US)
|
||||
val nameAsData = onsName.toByteArray()
|
||||
val nameHash = ByteArray(GenericHash.BYTES)
|
||||
if (!sodium.cryptoGenericHash(nameHash, nameHash.size, nameAsData, nameAsData.size.toLong())) {
|
||||
@@ -290,9 +290,7 @@ object SnodeAPI {
|
||||
getTargetSnodes(destination).map { swarm ->
|
||||
swarm.map { snode ->
|
||||
val parameters = message.toJSON()
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
invoke(Snode.Method.SendMessage, snode, destination, parameters)
|
||||
}
|
||||
invoke(Snode.Method.SendMessage, snode, destination, parameters)
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
|
@@ -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.
|
||||
}
|
||||
}
|
@@ -779,4 +779,12 @@ object TextSecurePreferences {
|
||||
fun setLastOpenDate(context: Context) {
|
||||
setLongPreference(context, LAST_OPEN_DATE, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
fun hasSeenLinkPreviewSuggestionDialog(context: Context): Boolean {
|
||||
return getBooleanPreference(context, "has_seen_link_preview_suggestion_dialog", false)
|
||||
}
|
||||
|
||||
fun setHasSeenLinkPreviewSuggestionDialog(context: Context) {
|
||||
setBooleanPreference(context, "has_seen_link_preview_suggestion_dialog", true)
|
||||
}
|
||||
}
|
@@ -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