commit
e7843e3af9
@ -1,8 +1,8 @@
|
||||
package org.thoughtcrime.securesms.loki.fragments
|
||||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
import android.content.Context
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
|
||||
import org.thoughtcrime.securesms.util.ContactUtilities
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader
|
||||
|
@ -1,10 +1,9 @@
|
||||
package org.thoughtcrime.securesms.loki.activities
|
||||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.thoughtcrime.securesms.loki.views.UserView
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms.loki.activities
|
||||
package org.thoughtcrime.securesms.contacts
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
|
||||
import org.thoughtcrime.securesms.util.ContactUtilities
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader
|
||||
|
||||
class SelectContactsLoader(context: Context, val usersToExclude: Set<String>) : AsyncLoader<List<String>>(context) {
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.views
|
||||
package org.thoughtcrime.securesms.conversation.v2.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.utilities
|
||||
package org.thoughtcrime.securesms.conversation.v2.utilities
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.utilities
|
||||
package org.thoughtcrime.securesms.crypto
|
||||
|
||||
import android.content.Context
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.database
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.net.Uri
|
||||
import java.util.*
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.utilities
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.database.getStringOrNull
|
@ -1,10 +1,9 @@
|
||||
package org.thoughtcrime.securesms.loki.database
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.*
|
@ -1,12 +1,9 @@
|
||||
package org.thoughtcrime.securesms.loki.database
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
@ -1,9 +1,7 @@
|
||||
package org.thoughtcrime.securesms.loki.database
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.utilities.get
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.dialogs
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
@ -1,10 +1,11 @@
|
||||
package org.thoughtcrime.securesms.loki.viewmodel
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||
import org.thoughtcrime.securesms.util.State
|
||||
|
||||
typealias DefaultGroups = List<OpenGroupAPIV2.DefaultGroup>
|
||||
typealias GroupState = State<DefaultGroups>
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.activities
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
@ -1,10 +1,10 @@
|
||||
package org.thoughtcrime.securesms.loki.activities
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.thoughtcrime.securesms.loki.views.UserView
|
||||
import org.thoughtcrime.securesms.contacts.UserView
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.activities
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.os.Bundle
|
||||
import kotlinx.android.synthetic.main.activity_open_group_guidelines.*
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.utilities
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.dialogs
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.activities
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
@ -1,71 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.*
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
|
||||
/**
|
||||
* Delegates the [OpenGroupUtilities.updateGroupInfo] call to the work manager.
|
||||
*/
|
||||
class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "PublicChatInfoUpdateWorker"
|
||||
|
||||
private const val DATA_KEY_SERVER_URL = "server_uRL"
|
||||
private const val DATA_KEY_CHANNEL = "channel"
|
||||
private const val DATA_KEY_ROOM = "room"
|
||||
|
||||
@JvmStatic
|
||||
fun scheduleInstant(context: Context, serverUrl: String, room :String) {
|
||||
val workRequest = OneTimeWorkRequestBuilder<PublicChatInfoUpdateWorker>()
|
||||
.setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.setInputData(workDataOf(
|
||||
DATA_KEY_SERVER_URL to serverUrl,
|
||||
DATA_KEY_ROOM to room
|
||||
))
|
||||
.build()
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.enqueue(workRequest)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun scheduleInstant(context: Context, serverURL: String, channel: Long) {
|
||||
val workRequest = OneTimeWorkRequestBuilder<PublicChatInfoUpdateWorker>()
|
||||
.setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.setInputData(workDataOf(
|
||||
DATA_KEY_SERVER_URL to serverURL,
|
||||
DATA_KEY_CHANNEL to channel
|
||||
))
|
||||
.build()
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.enqueue(workRequest)
|
||||
}
|
||||
}
|
||||
|
||||
override fun doWork(): Result {
|
||||
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
|
||||
val room = inputData.getString(DATA_KEY_ROOM)
|
||||
val openGroupId = "$serverUrl.$room"
|
||||
return try {
|
||||
Log.v(TAG, "Updating open group info for $openGroupId.")
|
||||
OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!)
|
||||
Log.v(TAG, "Open group info was successfully updated for $openGroupId.")
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to update open group info for $openGroupId", e)
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.dialogs
|
||||
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.fragment_device_list_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
|
||||
public class DeviceEditingOptionsBottomSheet : BottomSheetDialogFragment() {
|
||||
var onEditTapped: (() -> Unit)? = null
|
||||
var onUnlinkTapped: (() -> Unit)? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_device_list_bottom_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
editDisplayNameText.setOnClickListener { onEditTapped?.invoke() }
|
||||
unlinkDeviceText.setOnClickListener { onUnlinkTapped?.invoke() }
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import android.content.ContentValues
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.loki.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.utilities.getAll
|
||||
import org.thoughtcrime.securesms.loki.utilities.getString
|
||||
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
|
||||
import org.session.libsignal.utilities.Hex
|
||||
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.PublicKeyValidation
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import java.util.*
|
||||
|
||||
object ClosedGroupsMigration {
|
||||
|
||||
public val closedGroupPublicKey = "closed_group_public_key"
|
||||
// Ratchets
|
||||
private val oldClosedGroupRatchetTable = "old_closed_group_ratchet_table"
|
||||
private val currentClosedGroupRatchetTable = "closed_group_ratchet_table"
|
||||
private val senderPublicKey = "sender_public_key"
|
||||
private val chainKey = "chain_key"
|
||||
private val keyIndex = "key_index"
|
||||
private val messageKeys = "message_keys"
|
||||
@JvmStatic val createOldClosedGroupRatchetTableCommand
|
||||
= "CREATE TABLE $oldClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
|
||||
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
|
||||
// Private keys
|
||||
@JvmStatic val createCurrentClosedGroupRatchetTableCommand
|
||||
= "CREATE TABLE $currentClosedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
|
||||
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
|
||||
// Private keys
|
||||
public val closedGroupPrivateKeyTable = "closed_group_private_key_table"
|
||||
public val closedGroupPrivateKey = "closed_group_private_key"
|
||||
@JvmStatic val createClosedGroupPrivateKeyTableCommand
|
||||
= "CREATE TABLE $closedGroupPrivateKeyTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);"
|
||||
|
||||
|
||||
fun perform(database: net.sqlcipher.database.SQLiteDatabase) {
|
||||
val publicKeys = database.getAll(closedGroupPrivateKeyTable, null, null) { cursor ->
|
||||
cursor.getString(closedGroupPublicKey)
|
||||
}.filter {
|
||||
PublicKeyValidation.isValid(it)
|
||||
}
|
||||
val keyPairs = mutableListOf<ECKeyPair>()
|
||||
for (publicKey in publicKeys) {
|
||||
val query = "${closedGroupPublicKey} = ?"
|
||||
val privateKey = database.get(closedGroupPrivateKeyTable, query, arrayOf( publicKey )) { cursor ->
|
||||
cursor.getString(closedGroupPrivateKey)
|
||||
}
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(Hex.fromStringCondensed(publicKey.removing05PrefixIfNeeded())), DjbECPrivateKey(Hex.fromStringCondensed(privateKey)))
|
||||
keyPairs.add(keyPair)
|
||||
val row = ContentValues(1)
|
||||
row.put(LokiAPIDatabase.groupPublicKey, publicKey)
|
||||
database.insertOrUpdate(LokiAPIDatabase.closedGroupPublicKeysTable, row, "${LokiAPIDatabase.groupPublicKey} = ?", arrayOf( publicKey ))
|
||||
}
|
||||
for (keyPair in keyPairs) {
|
||||
// In this particular case keyPair.publicKey == groupPublicKey
|
||||
val timestamp = Date().time.toString()
|
||||
val index = "${keyPair.publicKey.serialize().toHexString()}-$timestamp"
|
||||
val encryptionKeyPairPublicKey = keyPair.publicKey.serialize().toHexString().removing05PrefixIfNeeded()
|
||||
val encryptionKeyPairPrivateKey = keyPair.privateKey.serialize().toHexString()
|
||||
val row = ContentValues(3)
|
||||
row.put(LokiAPIDatabase.closedGroupsEncryptionKeyPairIndex, index)
|
||||
row.put(LokiAPIDatabase.encryptionKeyPairPublicKey, encryptionKeyPairPublicKey)
|
||||
row.put(LokiAPIDatabase.encryptionKeyPairPrivateKey, encryptionKeyPairPrivateKey)
|
||||
database.insertOrUpdate(LokiAPIDatabase.closedGroupEncryptionKeyPairsTable, row, "${LokiAPIDatabase.closedGroupsEncryptionKeyPairIndex} = ?", arrayOf( index ))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,392 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.sending_receiving.*
|
||||
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.messages.SignalServiceGroup
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
||||
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
import java.util.*
|
||||
|
||||
object ClosedGroupsProtocolV2 {
|
||||
|
||||
@JvmStatic
|
||||
fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
|
||||
when (closedGroupUpdate.type) {
|
||||
DataMessage.ClosedGroupControlMessage.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey, sentTimestamp)
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> handleClosedGroupMemberLeft(context, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, groupPublicKey, senderPublicKey)
|
||||
else -> {
|
||||
Log.d("Loki","Can't handle closed group update of unknown type: ${closedGroupUpdate.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValid(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, senderPublicKey: String, sentTimestamp: Long): Boolean {
|
||||
val record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, senderPublicKey)
|
||||
if (record != null) return false
|
||||
|
||||
return when (closedGroupUpdate.type) {
|
||||
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
|
||||
(!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
|
||||
&& !(closedGroupUpdate.encryptionKeyPair.publicKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED,
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED -> {
|
||||
closedGroupUpdate.membersCount > 0
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> {
|
||||
senderPublicKey.isNotEmpty()
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> {
|
||||
!closedGroupUpdate.name.isNullOrEmpty()
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
public fun handleNewClosedGroup(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, senderPublicKey: String, sentTimestamp: Long) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
// Unwrap the message
|
||||
val groupPublicKey = closedGroupUpdate.publicKey.toByteArray().toHexString()
|
||||
val name = closedGroupUpdate.name
|
||||
val encryptionKeyPairAsProto = closedGroupUpdate.encryptionKeyPair
|
||||
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() }
|
||||
// Create the group
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val prevGroup = groupDB.getGroup(groupID).orNull()
|
||||
if (prevGroup != null) {
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
groupDB.create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), sentTimestamp)
|
||||
}
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
apiDB.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user (if we didn't make the group)
|
||||
if (userPublicKey != senderPublicKey) {
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
} else if (prevGroup == null) {
|
||||
// only notify if we created this group
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
}
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
}
|
||||
|
||||
fun handleClosedGroupMembersRemoved(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.toString() }
|
||||
|
||||
// Users that are part of this remove update
|
||||
val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
// If admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.any { it in updateMembers }
|
||||
// newMembers to save is old members minus removed members
|
||||
val newMembers = members - updateMembers
|
||||
// user should be posting MEMBERS_LEFT so this should not be encountered
|
||||
val senderLeft = senderPublicKey in updateMembers
|
||||
if (senderLeft) {
|
||||
Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey")
|
||||
}
|
||||
val wasCurrentUserRemoved = userPublicKey in updateMembers
|
||||
|
||||
// admin should send a MEMBERS_LEFT message but handled here in case
|
||||
if (didAdminLeave || wasCurrentUserRemoved) {
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
} else {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
|
||||
}
|
||||
}
|
||||
val type =
|
||||
if (senderLeft) SignalServiceGroup.Type.QUIT
|
||||
else SignalServiceGroup.Type.UPDATE
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
// Check common group update logic
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
// Users that are part of this add update
|
||||
val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
// newMembers to save is old members plus members included in this update
|
||||
val newMembers = members + updateMembers
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
}
|
||||
if (userPublicKey in admins) {
|
||||
// send current encryption key to the latest added members
|
||||
val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull()
|
||||
?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
// Check common group update logic
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = closedGroupUpdate.name
|
||||
groupDB.updateTitle(groupID, name)
|
||||
// Notify the user
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Check the user leaving isn't us, will already be handled
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.toString() }
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
// If the admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.contains(senderPublicKey)
|
||||
val updatedMemberList = members - senderPublicKey
|
||||
val userLeft = (userPublicKey == senderPublicKey)
|
||||
|
||||
// if the admin left, we left, or we are the only remaining member: remove the group
|
||||
if (didAdminLeave || userLeft) {
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
} else {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
||||
}
|
||||
}
|
||||
// Notify user
|
||||
if (userLeft) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) {
|
||||
apiDB.removeClosedGroupPublicKey(groupPublicKey)
|
||||
// Remove the key pairs
|
||||
apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
// Mark the group as inactive
|
||||
groupDB.setActive(groupID, false)
|
||||
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
}
|
||||
|
||||
private fun isValidGroupUpdate(group: GroupRecord,
|
||||
sentTimestamp: Long,
|
||||
senderPublicKey: String): Boolean {
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check that the message isn't from before the group was created
|
||||
// TODO: We should check that formationTimestamp is the sent timestamp of the closed group update that created the group
|
||||
if (group.formationTimestamp > sentTimestamp) {
|
||||
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) {
|
||||
Log.d("Loki", "Ignoring closed group info message from non-member.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleGroupEncryptionKeyPair(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val userKeyPair = apiDB.getUserX25519KeyPair()
|
||||
// Unwrap the message
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupPublicKeyToUse = when {
|
||||
groupPublicKey.isNotEmpty() -> groupPublicKey
|
||||
!closedGroupUpdate.publicKey.isEmpty -> closedGroupUpdate.publicKey.toByteArray().toHexString()
|
||||
else -> ""
|
||||
}
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKeyToUse)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
|
||||
Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.")
|
||||
return
|
||||
}
|
||||
// Find our wrapper and decrypt it if possible
|
||||
val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return
|
||||
val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray()
|
||||
val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first
|
||||
// Parse it
|
||||
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
|
||||
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
|
||||
// Store it
|
||||
apiDB.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKeyToUse)
|
||||
Log.d("Loki", "Received a new closed group encryption key pair")
|
||||
}
|
||||
|
||||
// region Deprecated
|
||||
private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
// Unwrap the message
|
||||
val name = closedGroupUpdate.name
|
||||
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null || !group.isActive) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
|
||||
return
|
||||
}
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check common group update logic
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
// Check that the admin wasn't removed unless the group was destroyed entirely
|
||||
if (!members.contains(group.admins.first().toString()) && members.isNotEmpty()) {
|
||||
Log.d("Loki", "Ignoring invalid closed group update message.")
|
||||
return
|
||||
}
|
||||
// Remove the group from the user's set of public keys to poll for if the current user was removed
|
||||
val wasCurrentUserRemoved = !members.contains(userPublicKey)
|
||||
if (wasCurrentUserRemoved) {
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
}
|
||||
// Generate and distribute a new encryption key pair if needed
|
||||
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
|
||||
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
|
||||
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
if (!wasCurrentUserRemoved) {
|
||||
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
val wasSenderRemoved = !members.contains(senderPublicKey)
|
||||
val type = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
|
||||
val admins = group.admins.map { it.toString() }
|
||||
if (userPublicKey == senderPublicKey) {
|
||||
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
|
||||
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp)
|
||||
} else {
|
||||
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -1,335 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.*
|
||||
import network.loki.messenger.R
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob
|
||||
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AudioViewKt"
|
||||
}
|
||||
|
||||
private val controlToggle: AnimatingToggle
|
||||
private val container: ViewGroup
|
||||
private val playButton: ImageView
|
||||
private val pauseButton: ImageView
|
||||
private val downloadButton: ImageView
|
||||
private val downloadProgress: ProgressBar
|
||||
private val seekBar: WaveformSeekBar
|
||||
private val totalDuration: TextView
|
||||
|
||||
private var downloadListener: SlideClickListener? = null
|
||||
private var audioSlidePlayer: AudioSlidePlayer? = null
|
||||
|
||||
/** Background coroutine scope that is available when the view is attached to a window. */
|
||||
private var asyncCoroutineScope: CoroutineScope? = null
|
||||
|
||||
private val loadingAnimation: SeekBarLoadingAnimation
|
||||
|
||||
constructor(context: Context): this(context, null)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
|
||||
View.inflate(context, R.layout.message_audio_view, this)
|
||||
container = findViewById(R.id.audio_widget_container)
|
||||
controlToggle = findViewById(R.id.control_toggle)
|
||||
playButton = findViewById(R.id.play)
|
||||
pauseButton = findViewById(R.id.pause)
|
||||
downloadButton = findViewById(R.id.download)
|
||||
downloadProgress = findViewById(R.id.download_progress)
|
||||
seekBar = findViewById(R.id.seek)
|
||||
totalDuration = findViewById(R.id.total_duration)
|
||||
|
||||
playButton.setOnClickListener {
|
||||
try {
|
||||
Log.d(TAG, "playbutton onClick")
|
||||
if (audioSlidePlayer != null) {
|
||||
togglePlayToPause()
|
||||
|
||||
// Restart the playback if progress bar is nearly at the end.
|
||||
val progress = if (seekBar.progress < 0.99f) seekBar.progress.toDouble() else 0.0
|
||||
|
||||
audioSlidePlayer!!.play(progress)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
pauseButton.setOnClickListener {
|
||||
Log.d(TAG, "pausebutton onClick")
|
||||
if (audioSlidePlayer != null) {
|
||||
togglePauseToPlay()
|
||||
audioSlidePlayer!!.stop()
|
||||
}
|
||||
}
|
||||
seekBar.isEnabled = false
|
||||
seekBar.progressChangeListener = object : WaveformSeekBar.ProgressChangeListener {
|
||||
override fun onProgressChanged(waveformSeekBar: WaveformSeekBar, progress: Float, fromUser: Boolean) {
|
||||
if (fromUser && audioSlidePlayer != null) {
|
||||
synchronized(audioSlidePlayer!!) {
|
||||
audioSlidePlayer!!.seekTo(progress.toDouble())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.play_icon))
|
||||
pauseButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.pause_icon))
|
||||
playButton.background = ContextCompat.getDrawable(context, R.drawable.ic_circle_fill_white_48dp)
|
||||
pauseButton.background = ContextCompat.getDrawable(context, R.drawable.ic_circle_fill_white_48dp)
|
||||
|
||||
if (attrs != null) {
|
||||
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.MessageAudioView, 0, 0)
|
||||
setTint(typedArray.getColor(R.styleable.MessageAudioView_foregroundTintColor, Color.WHITE),
|
||||
typedArray.getColor(R.styleable.MessageAudioView_waveformFillColor, Color.WHITE),
|
||||
typedArray.getColor(R.styleable.MessageAudioView_waveformBackgroundColor, Color.WHITE))
|
||||
container.setBackgroundColor(typedArray.getColor(R.styleable.MessageAudioView_widgetBackground, Color.TRANSPARENT))
|
||||
typedArray.recycle()
|
||||
}
|
||||
|
||||
loadingAnimation = SeekBarLoadingAnimation(this, seekBar)
|
||||
loadingAnimation.start()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this)
|
||||
|
||||
asyncCoroutineScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
EventBus.getDefault().unregister(this)
|
||||
|
||||
// Cancel all the background operations.
|
||||
asyncCoroutineScope!!.cancel()
|
||||
asyncCoroutineScope = null
|
||||
}
|
||||
|
||||
fun setAudio(audio: AudioSlide, showControls: Boolean) {
|
||||
when {
|
||||
showControls && audio.isPendingDownload -> {
|
||||
controlToggle.displayQuick(downloadButton)
|
||||
seekBar.isEnabled = false
|
||||
downloadButton.setOnClickListener { v -> downloadListener?.onClick(v, audio) }
|
||||
if (downloadProgress.isIndeterminate) {
|
||||
downloadProgress.isIndeterminate = false
|
||||
downloadProgress.progress = 0
|
||||
}
|
||||
}
|
||||
(showControls && audio.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) -> {
|
||||
controlToggle.displayQuick(downloadProgress)
|
||||
seekBar.isEnabled = false
|
||||
downloadProgress.isIndeterminate = true
|
||||
}
|
||||
else -> {
|
||||
controlToggle.displayQuick(playButton)
|
||||
seekBar.isEnabled = true
|
||||
if (downloadProgress.isIndeterminate) {
|
||||
downloadProgress.isIndeterminate = false
|
||||
downloadProgress.progress = 100
|
||||
}
|
||||
|
||||
// Post to make sure it executes only when the view is attached to a window.
|
||||
post(::updateFromAttachmentAudioExtras)
|
||||
}
|
||||
}
|
||||
audioSlidePlayer = AudioSlidePlayer.createFor(context, audio, this)
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
if (audioSlidePlayer != null && pauseButton.visibility == View.VISIBLE) {
|
||||
audioSlidePlayer!!.stop()
|
||||
}
|
||||
}
|
||||
|
||||
fun setDownloadClickListener(listener: SlideClickListener?) {
|
||||
downloadListener = listener
|
||||
}
|
||||
|
||||
fun setTint(@ColorInt foregroundTint: Int, @ColorInt waveformFill: Int, @ColorInt waveformBackground: Int) {
|
||||
playButton.backgroundTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.white, context.theme))
|
||||
playButton.imageTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme))
|
||||
pauseButton.backgroundTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.white, context.theme))
|
||||
pauseButton.imageTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme))
|
||||
|
||||
downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN)
|
||||
|
||||
downloadProgress.backgroundTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.white, context.theme))
|
||||
downloadProgress.progressTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme))
|
||||
downloadProgress.indeterminateTintList = ColorStateList.valueOf(resources.getColorWithID(R.color.black, context.theme))
|
||||
|
||||
totalDuration.setTextColor(foregroundTint)
|
||||
|
||||
seekBar.barProgressColor = waveformFill
|
||||
seekBar.barBackgroundColor = waveformBackground
|
||||
}
|
||||
|
||||
override fun onPlayerStart(player: AudioSlidePlayer) {
|
||||
if (pauseButton.visibility != View.VISIBLE) {
|
||||
togglePlayToPause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayerStop(player: AudioSlidePlayer) {
|
||||
if (playButton.visibility != View.VISIBLE) {
|
||||
togglePauseToPlay()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, millis: Long) {
|
||||
seekBar.progress = progress.toFloat()
|
||||
}
|
||||
|
||||
override fun setFocusable(focusable: Boolean) {
|
||||
super.setFocusable(focusable)
|
||||
playButton.isFocusable = focusable
|
||||
pauseButton.isFocusable = focusable
|
||||
seekBar.isFocusable = focusable
|
||||
seekBar.isFocusableInTouchMode = focusable
|
||||
downloadButton.isFocusable = focusable
|
||||
}
|
||||
|
||||
override fun setClickable(clickable: Boolean) {
|
||||
super.setClickable(clickable)
|
||||
playButton.isClickable = clickable
|
||||
pauseButton.isClickable = clickable
|
||||
seekBar.isClickable = clickable
|
||||
seekBar.setOnTouchListener(if (clickable) null else
|
||||
OnTouchListener { _, _ -> return@OnTouchListener true }) // Suppress touch events.
|
||||
downloadButton.isClickable = clickable
|
||||
}
|
||||
|
||||
override fun setEnabled(enabled: Boolean) {
|
||||
super.setEnabled(enabled)
|
||||
playButton.isEnabled = enabled
|
||||
pauseButton.isEnabled = enabled
|
||||
downloadButton.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun togglePlayToPause() {
|
||||
controlToggle.displayQuick(pauseButton)
|
||||
val playToPauseDrawable = ContextCompat.getDrawable(context, R.drawable.play_to_pause_animation) as AnimatedVectorDrawable
|
||||
pauseButton.setImageDrawable(playToPauseDrawable)
|
||||
playToPauseDrawable.start()
|
||||
}
|
||||
|
||||
private fun togglePauseToPlay() {
|
||||
controlToggle.displayQuick(playButton)
|
||||
val pauseToPlayDrawable = ContextCompat.getDrawable(context, R.drawable.pause_to_play_animation) as AnimatedVectorDrawable
|
||||
playButton.setImageDrawable(pauseToPlayDrawable)
|
||||
pauseToPlayDrawable.start()
|
||||
}
|
||||
|
||||
private fun obtainDatabaseAttachment(): DatabaseAttachment? {
|
||||
audioSlidePlayer ?: return null
|
||||
val attachment = audioSlidePlayer!!.audioSlide.asAttachment()
|
||||
return if (attachment is DatabaseAttachment) attachment else null
|
||||
}
|
||||
|
||||
private fun updateFromAttachmentAudioExtras() {
|
||||
val attachment = obtainDatabaseAttachment() ?: return
|
||||
|
||||
val audioExtras = DatabaseFactory.getAttachmentDatabase(context)
|
||||
.getAttachmentAudioExtras(attachment.attachmentId)
|
||||
|
||||
// Schedule a job request if no audio extras were generated yet.
|
||||
if (audioExtras == null) {
|
||||
ApplicationContext.getInstance(context).jobManager
|
||||
.add(PrepareAttachmentAudioExtrasJob(attachment.attachmentId))
|
||||
return
|
||||
}
|
||||
|
||||
loadingAnimation.stop()
|
||||
seekBar.sampleData = audioExtras.visualSamples
|
||||
|
||||
if (audioExtras.durationMs > 0) {
|
||||
totalDuration.visibility = View.VISIBLE
|
||||
totalDuration.text = String.format("%02d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs),
|
||||
TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs))
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
fun onEvent(event: PartProgressEvent) {
|
||||
if (audioSlidePlayer != null && event.attachment == audioSlidePlayer!!.audioSlide.asAttachment()) {
|
||||
val progress = ((event.progress.toFloat() / event.total) * 100f).toInt()
|
||||
downloadProgress.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onEvent(event: PrepareAttachmentAudioExtrasJob.AudioExtrasUpdatedEvent) {
|
||||
if (event.attachmentId == obtainDatabaseAttachment()?.attachmentId) {
|
||||
updateFromAttachmentAudioExtras()
|
||||
}
|
||||
}
|
||||
|
||||
private class SeekBarLoadingAnimation(
|
||||
private val hostView: View,
|
||||
private val seekBar: WaveformSeekBar): Runnable {
|
||||
|
||||
private var active = false
|
||||
|
||||
companion object {
|
||||
private const val UPDATE_PERIOD = 250L // In milliseconds.
|
||||
private val random = Random()
|
||||
}
|
||||
|
||||
fun start() {
|
||||
stop()
|
||||
active = true
|
||||
hostView.postDelayed(this, UPDATE_PERIOD)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
active = false
|
||||
hostView.removeCallbacks(this)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (!active) return
|
||||
|
||||
// Generate a random samples with values up to the 50% of the maximum value.
|
||||
seekBar.sampleData = ByteArray(PrepareAttachmentAudioExtrasJob.VISUAL_RMS_FRAMES)
|
||||
{ (random.nextInt(127) - 64).toByte() }
|
||||
hostView.postDelayed(this, UPDATE_PERIOD)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
|
||||
class OpenGroupInvitationView : FrameLayout {
|
||||
private val joinButton: ImageView
|
||||
private val openGroupIconContainer: RelativeLayout
|
||||
private val openGroupIconImageView: ImageView
|
||||
private val nameTextView: TextView
|
||||
private val urlTextView: TextView
|
||||
private var url: String = ""
|
||||
|
||||
constructor(context: Context): this(context, null)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) {
|
||||
View.inflate(context, R.layout.open_group_invitation_view, this)
|
||||
joinButton = findViewById(R.id.join_open_group_button)
|
||||
openGroupIconContainer = findViewById(R.id.open_group_icon_image_view_container)
|
||||
openGroupIconImageView = findViewById(R.id.open_group_icon_image_view)
|
||||
nameTextView = findViewById(R.id.name_text_view)
|
||||
urlTextView = findViewById(R.id.url_text_view)
|
||||
joinButton.setOnClickListener { joinOpenGroup(url) }
|
||||
}
|
||||
|
||||
fun setOpenGroup(name: String, url: String, isOutgoing: Boolean = false) {
|
||||
nameTextView.text = name
|
||||
urlTextView.text = OpenGroupUrlParser.trimQueryParameter(url)
|
||||
this.url = url
|
||||
joinButton.visibility = if (isOutgoing) View.GONE else View.VISIBLE
|
||||
openGroupIconContainer.visibility = if (isOutgoing) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun joinOpenGroup(url: String) {
|
||||
val openGroup = OpenGroupUrlParser.parseUrl(url)
|
||||
val builder = AlertDialog.Builder(context)
|
||||
builder.setTitle(context.getString(R.string.ConversationActivity_join_open_group, nameTextView.text.toString()))
|
||||
builder.setCancelable(true)
|
||||
val message: String =
|
||||
context.getString(R.string.ConversationActivity_join_open_group_confirmation_message, nameTextView.text.toString())
|
||||
builder.setMessage(message)
|
||||
builder.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
dialog.dismiss()
|
||||
OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, context)
|
||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(context)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Failed to join open group.", e)
|
||||
Toast.makeText(context, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.setNegativeButton(R.string.no, null)
|
||||
builder.show()
|
||||
}
|
||||
}
|
@ -1,315 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.views
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.core.math.MathUtils
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.byteToNormalizedFloat
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class WaveformSeekBar : View {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun dp(context: Context, dp: Float): Float {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
dp,
|
||||
context.resources.displayMetrics
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val sampleDataHolder = SampleDataHolder(::invalidate)
|
||||
/** An array of signed byte values representing the audio signal. */
|
||||
var sampleData: ByteArray?
|
||||
get() {
|
||||
return sampleDataHolder.getSamples()
|
||||
}
|
||||
set(value) {
|
||||
sampleDataHolder.setSamples(value)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/** Indicates whether the user is currently interacting with the view and performing a seeking gesture. */
|
||||
private var userSeeking = false
|
||||
private var _progress: Float = 0f
|
||||
/** In [0..1] range. */
|
||||
var progress: Float
|
||||
set(value) {
|
||||
// Do not let to modify the progress value from the outside
|
||||
// when the user is currently interacting with the view.
|
||||
if (userSeeking) return
|
||||
|
||||
_progress = value
|
||||
invalidate()
|
||||
progressChangeListener?.onProgressChanged(this, _progress, false)
|
||||
}
|
||||
get() {
|
||||
return _progress
|
||||
}
|
||||
|
||||
var barBackgroundColor: Int = Color.LTGRAY
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var barProgressColor: Int = Color.WHITE
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var barGap: Float = dp(context, 2f)
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var barWidth: Float = dp(context, 5f)
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var barMinHeight: Float = barWidth
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var barCornerRadius: Float = dp(context, 2.5f)
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var barGravity: WaveGravity = WaveGravity.CENTER
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var progressChangeListener: ProgressChangeListener? = null
|
||||
|
||||
private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val barRect = RectF()
|
||||
|
||||
private var canvasWidth = 0
|
||||
private var canvasHeight = 0
|
||||
|
||||
private var touchDownX = 0f
|
||||
private var touchDownProgress: Float = 0f
|
||||
private var scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
|
||||
: super(context, attrs, defStyleAttr) {
|
||||
|
||||
val typedAttrs = context.obtainStyledAttributes(attrs, R.styleable.WaveformSeekBar)
|
||||
barWidth = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_width, barWidth)
|
||||
barGap = typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_gap, barGap)
|
||||
barCornerRadius = typedAttrs.getDimension(
|
||||
R.styleable.WaveformSeekBar_bar_corner_radius,
|
||||
barCornerRadius)
|
||||
barMinHeight =
|
||||
typedAttrs.getDimension(R.styleable.WaveformSeekBar_bar_min_height, barMinHeight)
|
||||
barBackgroundColor = typedAttrs.getColor(
|
||||
R.styleable.WaveformSeekBar_bar_background_color,
|
||||
barBackgroundColor)
|
||||
barProgressColor =
|
||||
typedAttrs.getColor(R.styleable.WaveformSeekBar_bar_progress_color, barProgressColor)
|
||||
progress = typedAttrs.getFloat(R.styleable.WaveformSeekBar_progress, progress)
|
||||
barGravity = WaveGravity.fromString(
|
||||
typedAttrs.getString(R.styleable.WaveformSeekBar_bar_gravity))
|
||||
|
||||
typedAttrs.recycle()
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
canvasWidth = w
|
||||
canvasHeight = h
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
val totalWidth = getAvailableWidth()
|
||||
val barAmount = (totalWidth / (barWidth + barGap)).toInt()
|
||||
|
||||
var lastBarRight = paddingLeft.toFloat()
|
||||
|
||||
(0 until barAmount).forEach { barIdx ->
|
||||
// Convert a signed byte to a [0..1] float.
|
||||
val barValue = byteToNormalizedFloat(sampleDataHolder.computeBarValue(barIdx, barAmount))
|
||||
|
||||
val barHeight = max(barMinHeight, getAvailableHeight() * barValue)
|
||||
|
||||
val top: Float = when (barGravity) {
|
||||
WaveGravity.TOP -> paddingTop.toFloat()
|
||||
WaveGravity.CENTER -> paddingTop + getAvailableHeight() * 0.5f - barHeight * 0.5f
|
||||
WaveGravity.BOTTOM -> canvasHeight - paddingBottom - barHeight
|
||||
}
|
||||
|
||||
barRect.set(lastBarRight, top, lastBarRight + barWidth, top + barHeight)
|
||||
|
||||
barPaint.color = if (barRect.right <= totalWidth * progress)
|
||||
barProgressColor else barBackgroundColor
|
||||
|
||||
canvas.drawRoundRect(barRect, barCornerRadius, barCornerRadius, barPaint)
|
||||
|
||||
lastBarRight = barRect.right + barGap
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (!isEnabled) return false
|
||||
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
userSeeking = true
|
||||
touchDownX = event.x
|
||||
touchDownProgress = progress
|
||||
updateProgress(event, false)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
// Prevent any parent scrolling if the user scrolled more
|
||||
// than scaledTouchSlop on horizontal axis.
|
||||
if (abs(event.x - touchDownX) > scaledTouchSlop) {
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
}
|
||||
updateProgress(event, false)
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
userSeeking = false
|
||||
updateProgress(event, true)
|
||||
performClick()
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
updateProgress(touchDownProgress, false)
|
||||
userSeeking = false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateProgress(event: MotionEvent, notify: Boolean) {
|
||||
updateProgress(event.x / getAvailableWidth(), notify)
|
||||
}
|
||||
|
||||
private fun updateProgress(progress: Float, notify: Boolean) {
|
||||
_progress = MathUtils.clamp(progress, 0f, 1f)
|
||||
invalidate()
|
||||
|
||||
if (notify) {
|
||||
progressChangeListener?.onProgressChanged(this, _progress, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
super.performClick()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getAvailableWidth() = canvasWidth - paddingLeft - paddingRight
|
||||
private fun getAvailableHeight() = canvasHeight - paddingTop - paddingBottom
|
||||
|
||||
private class SampleDataHolder(private val invalidateDelegate: () -> Any) {
|
||||
|
||||
private var sampleDataFrom: ByteArray? = null
|
||||
private var sampleDataTo: ByteArray? = null
|
||||
private var progress = 1f // Mix between from and to values.
|
||||
|
||||
private var animation: ValueAnimator? = null
|
||||
|
||||
fun computeBarValue(barIdx: Int, barAmount: Int): Byte {
|
||||
/** @return The array's value at the interpolated index. */
|
||||
fun getSampleValue(sampleData: ByteArray?): Byte {
|
||||
if (sampleData == null || sampleData.isEmpty())
|
||||
return Byte.MIN_VALUE
|
||||
else {
|
||||
val sampleIdx = (barIdx * (sampleData.size / barAmount.toFloat())).toInt()
|
||||
return sampleData[sampleIdx]
|
||||
}
|
||||
}
|
||||
|
||||
if (progress == 1f) {
|
||||
return getSampleValue(sampleDataTo)
|
||||
}
|
||||
|
||||
val fromValue = getSampleValue(sampleDataFrom)
|
||||
val toValue = getSampleValue(sampleDataTo)
|
||||
val rawResultValue = fromValue * (1f - progress) + toValue * progress
|
||||
return rawResultValue.roundToInt().toByte()
|
||||
}
|
||||
|
||||
fun setSamples(sampleData: ByteArray?) {
|
||||
/** @return a mix between [sampleDataFrom] and [sampleDataTo] arrays according to the current [progress] value. */
|
||||
fun computeNewDataFromArray(): ByteArray? {
|
||||
if (sampleDataTo == null) return null
|
||||
if (sampleDataFrom == null) return sampleDataTo
|
||||
|
||||
val sampleSize = min(sampleDataFrom!!.size, sampleDataTo!!.size)
|
||||
return ByteArray(sampleSize) { i -> computeBarValue(i, sampleSize) }
|
||||
}
|
||||
|
||||
sampleDataFrom = computeNewDataFromArray()
|
||||
sampleDataTo = sampleData
|
||||
progress = 0f
|
||||
|
||||
animation?.cancel()
|
||||
animation = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
addUpdateListener { animation ->
|
||||
progress = animation.animatedValue as Float
|
||||
invalidateDelegate()
|
||||
}
|
||||
interpolator = DecelerateInterpolator(3f)
|
||||
duration = 500
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSamples(): ByteArray? {
|
||||
return sampleDataTo
|
||||
}
|
||||
}
|
||||
|
||||
enum class WaveGravity {
|
||||
TOP,
|
||||
CENTER,
|
||||
BOTTOM,
|
||||
;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromString(gravity: String?): WaveGravity = when (gravity) {
|
||||
"1" -> TOP
|
||||
"2" -> CENTER
|
||||
else -> BOTTOM
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ProgressChangeListener {
|
||||
fun onProgressChanged(waveformSeekBar: WaveformSeekBar, progress: Float, fromUser: Boolean)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
@ -1,5 +1,5 @@
|
||||
@file:JvmName("FcmUtils")
|
||||
package org.thoughtcrime.securesms.loki.utilities
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
package org.thoughtcrime.securesms.notifications
|
||||
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.functional.map
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue