Integrate shared libsession-util library (#1096)
* feat: add some config db basics and DI for it, make the user profile optional, start looking at integrate building from initial dump * update: get latest util library submodule update * refactor: fix compile for refactored API * refactor: naming consistent with library * feat: add in config storage and injection to common places, managing lifecycle of native instances * refactor: config database changes, new protos, adding in support for config base namespace queries * refactor: config query and store use the same format as other platforms * feat: add batch snode calls and try to poll from all the config namespaces * fix: add optional namespace in signature and params * feat: add raw requests for modifying expiry and getting expiries * feat: add some base config migration logic, start implementing wrappers for conversation and expiry types * chore: update libsession base * feat: start integrating conversation wrapper functions * feat: add basic conversation info volatile types and implementations, start working on tests * feat: more common library wrapper implementation and test * fix: tests and compile issues * fix: fix tests, don't use iterables * feat: add all iterators and tests * feat: add in more config factory for volatile * feat: update request responses and their appropriate processing * feat: add storage with hashes and some basic profile update logic in config factory probably move that somewhere else * feat: adding config sync functionality, refactoring jobs to execute in suspend context to do some nice coroutine execution * refactor: moving some properties around so we have access in libsession * feat: expand on the config sync job, finish basic implementation to test against * feat: add forced config sync * feat: syncs the user profile stuff for now, and errors back to placeholder instead of unknown recipient * feat: add basic message read logic for synchronizing last reads, need to modify the query to use the last seen instead of the unread count in a subquery possibly for thread display record * feat: add broken unreads everywhere * fix: unreads work now for incoming messages, need to sync conv volatile properly still * feat: batching poll responses properly and handling groups properly * fix: replace the mark read receiver (from notifications) to use the new set last seen mark read logic * feat: update to the group list branch * fix: compile errors from updating library to use latest branch, now requires cmake 3.22.1 * fix: fix the contact tests * fix: getters weren't getters properly in the config factory, fixed new onboarding from configs * feat: add the last seen * feat: start adding user groups wrapper objects * refactor: add more else branches for unimplemented types * feat: buffer the last read when in conversation * feat: add basic contact logic for setting local contact state. Need to implement handling properly * refactor: trying to just include blocked status for now in updating contacts * fix: add some more contact syncing: nicknames, approved statuses, blocked statuses * feat: start implementing hashes in shared lib and refactoring * feat: start to implement group list info classes and wrappers and refactor to use library based hashes * feat: incorporate hashes from library, more wrapper for user groups and serialization from c++ * feat: adding more serialization changes for community base info and user groups LGC * feat: adding more serialize deserialize to legacy closed groups * feat: finish serial/deserial helper * feat: just implement deserialize community info * refactor: refactor tests and wrappers to use less pointers, finish implementing user groups API * feat: finish latest wrappers fix tests and continue building default generation functions. refactor defaults to be used if no stored data blob in DB * feat: more usergroup functionality, storage functionality for checking pinned status, adding pinned status for NTS/contacts, move community info parse full url to base community, add StorageProtocol logic for group info * feat: adding user groups to the list of user configs, refactorign some of the config factory to fetch the user configs easier. Add handling for polling user group namespace * feat: implement the default user config list * feat: add user group config handling * chore: extra missed existing group * refactor: use existing lookup for objects in wrappers so they don't overwrite missing values * feat: add contacts expiry serialization/deserialization, more LGC, timestamps to add closed group encryption info (for latest tracking) * refactor: change how expiration timer works for contacts, set the expiration timer for those conversations in handling contact configs * feat: add expiration updates via config for contacts as well * feat: add almost all group editing cases, need to hook into the thread deletion for groups in the user groups * feat: open group joining should work now * feat: add groups to configs for push * fix: handling user group updates bug fix for closed groups instead of all groups * fix: open group sync persistence * feat: add in activity finish if recipient no longer exists (deleted thread) from sync * feat: support avatar removal from shared library * feat: support thread deletion and refactoring a lot of getOrCreateThread references to go via storage or assume they are correctly set to hook into the contact and volatile creation during thread creation * fix: database update not deleting in certain circumstances, storage persisting and removing the volatile convo info for thread deletion / creation, NTS hidden getter values in shared library * refactor: make update listener visibility package * refactor: update kotlin * feat: update dependencies and support outdated config messages, refactor config factory to return null configs if new configs not supported * feat: update shared library to use priority only, fix compile errors, fix group member sync problem * fix: compile error * fix: profile avatar fixes for local user now that we aren't setting local user profile key * Revert "fix: profile avatar fixes for local user now that we aren't setting local user profile key" This reverts commitpull/1277/head3f569e3403
. * refactor: let the local number update recipient details in profile manager * fix: don't recreate thread after leaving * fix: fix up the duplicate thread creation in the message receive handler * fix: fix the placeholder rendering on new messages, add in extra context logging for adding contacts and preventing new thread creation on new messages of various types * feat: add test theme for xml layout previews * feat: add shortened hex for session IDs throughout, replace nullable getName with null in underlying contacts for individual contacts, build shared lib with release mode, remove todo, fix broken unit test * feat: setup android unit tests for verifying storage behaviours and state of shared configs * feat: adding dependencies to try and get android tests working, fixing bug with initial config not syncing properly * fix: remove hilt testing, add spy on app context storage field instead, update libsession-util to fixed sodium cmake branch * refactor: use PR version of libsession-util to test cmake build * fix: new build on normal repo * feat: new libsession util commit * refactor: remove the old custom build libsodium stuff from cmake * feat: update libsession module * fix: add legacy config subscription to the home activity to enable showing banner at any time * fix: pinned status for communities and groups, group last read time being set to snodeapi.now on finish joining * fix: some open group volatile convo fix for last read timer being set. Need to investigate further * fix: prevent blocking local number * fix: adding in more checks for open group inbox recipients before being saved to the shared configs. Prevent sending typing indicator for blocked users * fix: add blocked check for read receipt and updating expiring messages * fix: another contact recipient config library call removed for non-standard IDs * fix: another ID check * fix: don't process thread creation for user is sender && recipient (sync message) for message request responses * refactor: mark as read on open and use less buffer time * fix: finally fix the darn unread count issue by * fix: removing debug logs, adding failure error handling logs for expiry message updater, properly using the message thread ID created for the expiring messages. Process the non-thread messages properly with await in BatchMessageReceiveJob * fix: checking the last read open to message and make sure that scroll behaviour matches expected, fix the config sync job not deleting ALL old hashes only latest * refactor: try to add a retry logic to config sync job in case of snode failure * build: update submodule * fix: remove user notifications for leaving group to prevent synced device issues, don't create thread in messages for new closed groups, includei nactive groups in the deletion queries for merging group configs * feat: use blinded message count for banner also * refactor: remove some logging, don't use blinded conversations in the list * fix: don't set the read flag in update notifications, some roundabout logic for first loads and scrolling to last known positions * refactor: merge changes, re-add the group check in unapproved messages * fix: re-poll on fail in case that was breaking anything * fix: pinning groups and notifying list listeners in threadDb.setPinned * feat: add in TTL extension subrequest and builder, enable extending TTLs for all latest config messages in poll as subrequest * feat: add block to the delete all message requests, only if they're not open group inbox contacts * refactor: disable edit text for non contacts * refactor: let the user display name return "You" for local user * fix: prevent NTS self create thread on user view bind * refactor: remove populate public key cache if needed call which seems unnecessary at that point, maybe UserView refs have changed since 2020 * refactor: use just first visible instead of completely visible, merge message sender changes * fix: prevent block of users in delete all * fix: self sync sync message failures for default values * feat: update libsession-util, adjust docs, update mms and sms to use message sent timestamp instead of -1 for last read in the thread * fix: some compile issues in tests and some TODOs for things to do before merge * fix: handle recyclerview scrolled on scroll to first unread if it's the first load * fix: added more migration code for deleting unnecessary threads and groups, fixed a post-migration last seen issue on last item (current read is now), comment out actual network sync while testing migrations * feat: adding a force new configs flag and logic for timestamp handling / forced configs, fix issue with handling legacy messages * refactor: re-add the sending of configs * fix: don't add contacts if they don't exist in the profile manager * [wip] fix: trying to consolidate prof pic and key properly * feat: add logs and fix compile issue with a themes.xml entry, add removing profile picture into logic for profile manager * fix: force has sent for local user, only prevent setting last seen for open group recipients, allow empty user pics to trigger config sync in settings * fix: nts threads * fix: open group avatar loop for open groups we have left * feat: add a wrapper hash to track home diff util changes for wrapper contact recipient info, add test for dirty state in double set * feat: add a dump in there as well * refactor: more test code refactor * fix: update last seen if later than current * fix: open group threads and avatar downloads * fix: add max size and maybe fix the non-200 sub requests for batches (for 421s in particular) * fix: open group comparison issues potentially, have to update some more outgoing message open group flags for visibility of details etc * Updated to the latest libSession-util * Updated logic to delete legacy groups when kicked/left * Added the legacy group 'joined_at' value * Replaced incorrect character in JNI * Fixed an issue where the group keyPair was getting encoded incorrectly * Updated the code to ignore outdated legacy group control message changes * Updated the code to ignore messages invalidated by the config * [Review] Updated the poller to process config messages before standard * Cleaned up the outdated message logic * Fixed inverted config dropping flags * Fixed an issue where the joining a community would read all messages Stopped using a reversed RecyclerView in all cases (caused the unread issue) Updated the logic to jump to the newly sent message when sending a message (to be consistent with other platforms) Updated the logic to refresh the DB unread count when the cursor receives an update * Updated the conversation to highlight the first unread message on open * Fixed a couple of bugs with the highlighting * Fixed a bug where the user profile picture wasn't downloading correctly * feat: add all namespaces to delete all messages request and signature verification data * fix: merge namespace hashes for signature returned and * fix: import correct scroll to bottom * build: update version code and name * fix: initial contact generation fix for existing blinded contacts * fix: initial convo generation fix for existing blinded convos (?) * fix: conversation unread not doing a check for standard ID prefix * fix: thread ID not being created for legacy config messages * fix: don't treat 404 as bad snode * fix: don't add retrieve profile job if we have one for that address * build: update build code * fix: reduce attempts for downloading image, invert unreachable type check * fix: attempting to fix preventing message processing if group thread is not active for closed groups and initial contact dump only allows conversations with thread, may need further optimisations though * feat: Added an unread marker and search result focus highlighting * fix: empty set in appropriate places for current closed groups * build: update build version code * fix: fix the notifications and request at appropriate time * refactor: remove debug logging for thread create and delete * build: update build number * fix: new community doesn't break persisting config if the .add request fails * build: trying to track down broken retrieve avatar job * feat: update to latest libsession dev * fix: maybe fix avatar download for new messages * fix: 404s causing snode errors and trying to retrieve avatars that have already 404'd a lot * fix: closed group creation sets thread date to formation timestamp * build: update version code * build: update version code * build: remove debuggable release build * fix: use new permissions for external attachments * build: update version code * chore: remove debug logs * fix: tests and main thread blocking db fetch for path status view * wip: trying to track down failure to mark conversation as read in delayed group add * wip: add more logs for initial last Read sync of communities * wip: maybe the volatile is being updated with 0 on batch message receive? * fix: maybe syncing read statuses are working now * chore: remove debug logs * build: update build number * fix: trying to improve performance * fix: add close to banner * refactor: hide seed reminder in preview * build: update build number * fix: maybe requires update thread no matter what * fix: message request banner shows again * fix: android tests work again and permissions * fix: blocked contacts click handler being overridden by something * Revert "fix: blocked contacts click handler being overridden by something" This reverts commit608572fc42
. * build: update build number * refactor: remove unused dependencies and update minor for sqlcipher * fix: actually do insert contact, because otherwise name doesn't get set properly * fix: maybe fix scroll to bottom issue * build: update build number * fix: the message time and jump to message queries are more optimized * fix: maybe fix the last seen issues * build: update build number * fix: pfp broken closed groups why * fix: add admins and members as member list instead of just members * fix: exclude lgc without membership > 1 and inactive explicitly * fix: submodule update * fix: compiles with removal of iterator erase * fix: unread indicator updates properly in ConversationActivityV2 * fix: unread notifications clear and altered if any notifications exist (prevents clearing read notifications in conversation or on home screen) * refactor: profile pictures kinda broken * build: update build number * refactor: remove full hash from log * fix: isPinned threadDB call * refactor: use mutex in all libsession native calls, change timestamp * refactor: add basic support for blinded v2 prefixes --------- Co-authored-by: Morgan Pretty <morgan.t.pretty@gmail.com>
parent
96ec733517
commit
ac18f1cbfe
@ -0,0 +1,3 @@
|
||||
[submodule "libsession-util/libsession-util"]
|
||||
path = libsession-util/libsession-util
|
||||
url = https://github.com/oxen-io/libsession-util.git
|
@ -0,0 +1,97 @@
|
||||
package network.loki.messenger
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.Contacts
|
||||
import network.loki.messenger.libsession_util.util.Contact
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.argThat
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.spy
|
||||
import org.mockito.kotlin.verify
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.KeyHelper
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@SmallTest
|
||||
class LibSessionTests {
|
||||
|
||||
private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||
private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
|
||||
private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
|
||||
|
||||
private var fakeHashI = 0
|
||||
private val nextFakeHash: String
|
||||
get() = "fakehash${fakeHashI++}"
|
||||
|
||||
private fun maybeGetUserInfo(): Pair<ByteArray, String>? {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||
val prefs = appContext.prefs
|
||||
val localUserPublicKey = prefs.getLocalNumber()
|
||||
val secretKey = with(appContext) {
|
||||
val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
|
||||
edKey.secretKey.asBytes
|
||||
}
|
||||
return if (localUserPublicKey == null || secretKey == null) null
|
||||
else secretKey to localUserPublicKey
|
||||
}
|
||||
|
||||
private fun buildContactMessage(contactList: List<Contact>): ByteArray {
|
||||
val (key,_) = maybeGetUserInfo()!!
|
||||
val contacts = Contacts.Companion.newInstance(key)
|
||||
contactList.forEach { contact ->
|
||||
contacts.set(contact)
|
||||
}
|
||||
return contacts.push().config
|
||||
}
|
||||
|
||||
private fun fakePollNewConfig(configBase: ConfigBase, toMerge: ByteArray) {
|
||||
configBase.merge(nextFakeHash to toMerge)
|
||||
MessagingModuleConfiguration.shared.configFactory.persist(configBase, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setupUser() {
|
||||
val newBytes = randomSeedBytes().toByteArray()
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
|
||||
val kp = KeyPairUtilities.generate(newBytes)
|
||||
KeyPairUtilities.store(context, kp.seed, kp.ed25519KeyPair, kp.x25519KeyPair)
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(context, registrationID)
|
||||
TextSecurePreferences.setLocalNumber(context, kp.x25519KeyPair.hexEncodedPublicKey)
|
||||
TextSecurePreferences.setRestorationTime(context, 0)
|
||||
TextSecurePreferences.setHasViewedSeed(context, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migration_one_to_ones() {
|
||||
val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||
val storageSpy = spy(app.storage)
|
||||
app.storage = storageSpy
|
||||
|
||||
val newContactId = randomSessionId()
|
||||
val singleContact = Contact(
|
||||
id = newContactId,
|
||||
approved = true,
|
||||
expiryMode = ExpiryMode.NONE
|
||||
)
|
||||
val newContactMerge = buildContactMessage(listOf(singleContact))
|
||||
val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
|
||||
fakePollNewConfig(contacts, newContactMerge)
|
||||
verify(storageSpy).addLibSessionContacts(argThat {
|
||||
first().let { it.id == newContactId && it.approved } && size == 1
|
||||
})
|
||||
verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.contentValuesOf
|
||||
import androidx.core.database.getBlobOrNull
|
||||
import androidx.core.database.getLongOrNull
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
|
||||
class ConfigDatabase(context: Context, helper: SQLCipherOpenHelper): Database(context, helper) {
|
||||
|
||||
companion object {
|
||||
private const val VARIANT = "variant"
|
||||
private const val PUBKEY = "publicKey"
|
||||
private const val DATA = "data"
|
||||
private const val TIMESTAMP = "timestamp" // Milliseconds
|
||||
|
||||
private const val TABLE_NAME = "configs_table"
|
||||
|
||||
const val CREATE_CONFIG_TABLE_COMMAND =
|
||||
"CREATE TABLE $TABLE_NAME ($VARIANT TEXT NOT NULL, $PUBKEY TEXT NOT NULL, $DATA BLOB, $TIMESTAMP INTEGER NOT NULL DEFAULT 0, PRIMARY KEY($VARIANT, $PUBKEY));"
|
||||
|
||||
private const val VARIANT_AND_PUBKEY_WHERE = "$VARIANT = ? AND $PUBKEY = ?"
|
||||
}
|
||||
|
||||
fun storeConfig(variant: String, publicKey: String, data: ByteArray, timestamp: Long) {
|
||||
val db = writableDatabase
|
||||
val contentValues = contentValuesOf(
|
||||
VARIANT to variant,
|
||||
PUBKEY to publicKey,
|
||||
DATA to data,
|
||||
TIMESTAMP to timestamp
|
||||
)
|
||||
db.insertOrUpdate(TABLE_NAME, contentValues, VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey))
|
||||
}
|
||||
|
||||
fun retrieveConfigAndHashes(variant: String, publicKey: String): ByteArray? {
|
||||
val db = readableDatabase
|
||||
val query = db.query(TABLE_NAME, arrayOf(DATA), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
|
||||
return query?.use { cursor ->
|
||||
if (!cursor.moveToFirst()) return@use null
|
||||
val bytes = cursor.getBlobOrNull(cursor.getColumnIndex(DATA)) ?: return@use null
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveConfigLastUpdateTimestamp(variant: String, publicKey: String): Long {
|
||||
val db = readableDatabase
|
||||
val cursor = db.query(TABLE_NAME, arrayOf(TIMESTAMP), VARIANT_AND_PUBKEY_WHERE, arrayOf(variant, publicKey),null, null, null)
|
||||
if (cursor == null) return 0
|
||||
if (!cursor.moveToFirst()) return 0
|
||||
return (cursor.getLongOrNull(cursor.getColumnIndex(TIMESTAMP)) ?: 0)
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Trace
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.Contacts
|
||||
import network.loki.messenger.libsession_util.ConversationVolatileConfig
|
||||
import network.loki.messenger.libsession_util.UserGroupsConfig
|
||||
import network.loki.messenger.libsession_util.UserProfile
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsession.utilities.ConfigFactoryUpdateListener
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.database.ConfigDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
|
||||
class ConfigFactory(
|
||||
private val context: Context,
|
||||
private val configDatabase: ConfigDatabase,
|
||||
private val maybeGetUserInfo: () -> Pair<ByteArray, String>?
|
||||
) :
|
||||
ConfigFactoryProtocol {
|
||||
companion object {
|
||||
// This is a buffer period within which we will process messages which would result in a
|
||||
// config change, any message which would normally result in a config change which was sent
|
||||
// before `lastConfigMessage.timestamp - configChangeBufferPeriod` will not actually have
|
||||
// it's changes applied (control text will still be added though)
|
||||
val configChangeBufferPeriod: Long = (2 * 60 * 1000)
|
||||
}
|
||||
|
||||
fun keyPairChanged() { // this should only happen restoring or clearing data
|
||||
_userConfig?.free()
|
||||
_contacts?.free()
|
||||
_convoVolatileConfig?.free()
|
||||
_userConfig = null
|
||||
_contacts = null
|
||||
_convoVolatileConfig = null
|
||||
}
|
||||
|
||||
private val userLock = Object()
|
||||
private var _userConfig: UserProfile? = null
|
||||
private val contactsLock = Object()
|
||||
private var _contacts: Contacts? = null
|
||||
private val convoVolatileLock = Object()
|
||||
private var _convoVolatileConfig: ConversationVolatileConfig? = null
|
||||
private val userGroupsLock = Object()
|
||||
private var _userGroups: UserGroupsConfig? = null
|
||||
|
||||
private val isConfigForcedOn = TextSecurePreferences.hasForcedNewConfig(context)
|
||||
|
||||
private val listeners: MutableList<ConfigFactoryUpdateListener> = mutableListOf()
|
||||
fun registerListener(listener: ConfigFactoryUpdateListener) {
|
||||
listeners += listener
|
||||
}
|
||||
|
||||
fun unregisterListener(listener: ConfigFactoryUpdateListener) {
|
||||
listeners -= listener
|
||||
}
|
||||
|
||||
private inline fun <T> synchronizedWithLog(lock: Any, body: ()->T): T {
|
||||
Trace.beginSection("synchronizedWithLog")
|
||||
val result = synchronized(lock) {
|
||||
body()
|
||||
}
|
||||
Trace.endSection()
|
||||
return result
|
||||
}
|
||||
|
||||
override val user: UserProfile?
|
||||
get() = synchronizedWithLog(userLock) {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
|
||||
if (_userConfig == null) {
|
||||
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
|
||||
val userDump = configDatabase.retrieveConfigAndHashes(
|
||||
SharedConfigMessage.Kind.USER_PROFILE.name,
|
||||
publicKey
|
||||
)
|
||||
_userConfig = if (userDump != null) {
|
||||
UserProfile.newInstance(secretKey, userDump)
|
||||
} else {
|
||||
ConfigurationMessageUtilities.generateUserProfileConfigDump()?.let { dump ->
|
||||
UserProfile.newInstance(secretKey, dump)
|
||||
} ?: UserProfile.newInstance(secretKey)
|
||||
}
|
||||
}
|
||||
_userConfig
|
||||
}
|
||||
|
||||
override val contacts: Contacts?
|
||||
get() = synchronizedWithLog(contactsLock) {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
|
||||
if (_contacts == null) {
|
||||
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
|
||||
val contactsDump = configDatabase.retrieveConfigAndHashes(
|
||||
SharedConfigMessage.Kind.CONTACTS.name,
|
||||
publicKey
|
||||
)
|
||||
_contacts = if (contactsDump != null) {
|
||||
Contacts.newInstance(secretKey, contactsDump)
|
||||
} else {
|
||||
ConfigurationMessageUtilities.generateContactConfigDump()?.let { dump ->
|
||||
Contacts.newInstance(secretKey, dump)
|
||||
} ?: Contacts.newInstance(secretKey)
|
||||
}
|
||||
}
|
||||
_contacts
|
||||
}
|
||||
|
||||
override val convoVolatile: ConversationVolatileConfig?
|
||||
get() = synchronizedWithLog(convoVolatileLock) {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
|
||||
if (_convoVolatileConfig == null) {
|
||||
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
|
||||
val convoDump = configDatabase.retrieveConfigAndHashes(
|
||||
SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name,
|
||||
publicKey
|
||||
)
|
||||
_convoVolatileConfig = if (convoDump != null) {
|
||||
ConversationVolatileConfig.newInstance(secretKey, convoDump)
|
||||
} else {
|
||||
ConfigurationMessageUtilities.generateConversationVolatileDump(context)
|
||||
?.let { dump ->
|
||||
ConversationVolatileConfig.newInstance(secretKey, dump)
|
||||
} ?: ConversationVolatileConfig.newInstance(secretKey)
|
||||
}
|
||||
}
|
||||
_convoVolatileConfig
|
||||
}
|
||||
|
||||
override val userGroups: UserGroupsConfig?
|
||||
get() = synchronizedWithLog(userGroupsLock) {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return null
|
||||
if (_userGroups == null) {
|
||||
val (secretKey, publicKey) = maybeGetUserInfo() ?: return null
|
||||
val userGroupsDump = configDatabase.retrieveConfigAndHashes(
|
||||
SharedConfigMessage.Kind.GROUPS.name,
|
||||
publicKey
|
||||
)
|
||||
_userGroups = if (userGroupsDump != null) {
|
||||
UserGroupsConfig.Companion.newInstance(secretKey, userGroupsDump)
|
||||
} else {
|
||||
ConfigurationMessageUtilities.generateUserGroupDump(context)?.let { dump ->
|
||||
UserGroupsConfig.Companion.newInstance(secretKey, dump)
|
||||
} ?: UserGroupsConfig.newInstance(secretKey)
|
||||
}
|
||||
}
|
||||
_userGroups
|
||||
}
|
||||
|
||||
override fun getUserConfigs(): List<ConfigBase> =
|
||||
listOfNotNull(user, contacts, convoVolatile, userGroups)
|
||||
|
||||
|
||||
private fun persistUserConfigDump(timestamp: Long) = synchronized(userLock) {
|
||||
val dumped = user?.dump() ?: return
|
||||
val (_, publicKey) = maybeGetUserInfo() ?: return
|
||||
configDatabase.storeConfig(SharedConfigMessage.Kind.USER_PROFILE.name, publicKey, dumped, timestamp)
|
||||
}
|
||||
|
||||
private fun persistContactsConfigDump(timestamp: Long) = synchronized(contactsLock) {
|
||||
val dumped = contacts?.dump() ?: return
|
||||
val (_, publicKey) = maybeGetUserInfo() ?: return
|
||||
configDatabase.storeConfig(SharedConfigMessage.Kind.CONTACTS.name, publicKey, dumped, timestamp)
|
||||
}
|
||||
|
||||
private fun persistConvoVolatileConfigDump(timestamp: Long) = synchronized(convoVolatileLock) {
|
||||
val dumped = convoVolatile?.dump() ?: return
|
||||
val (_, publicKey) = maybeGetUserInfo() ?: return
|
||||
configDatabase.storeConfig(
|
||||
SharedConfigMessage.Kind.CONVO_INFO_VOLATILE.name,
|
||||
publicKey,
|
||||
dumped,
|
||||
timestamp
|
||||
)
|
||||
}
|
||||
|
||||
private fun persistUserGroupsConfigDump(timestamp: Long) = synchronized(userGroupsLock) {
|
||||
val dumped = userGroups?.dump() ?: return
|
||||
val (_, publicKey) = maybeGetUserInfo() ?: return
|
||||
configDatabase.storeConfig(SharedConfigMessage.Kind.GROUPS.name, publicKey, dumped, timestamp)
|
||||
}
|
||||
|
||||
override fun persist(forConfigObject: ConfigBase, timestamp: Long) {
|
||||
try {
|
||||
listeners.forEach { listener ->
|
||||
listener.notifyUpdates(forConfigObject)
|
||||
}
|
||||
when (forConfigObject) {
|
||||
is UserProfile -> persistUserConfigDump(timestamp)
|
||||
is Contacts -> persistContactsConfigDump(timestamp)
|
||||
is ConversationVolatileConfig -> persistConvoVolatileConfigDump(timestamp)
|
||||
is UserGroupsConfig -> persistUserGroupsConfigDump(timestamp)
|
||||
else -> throw UnsupportedOperationException("Can't support type of ${forConfigObject::class.simpleName} yet")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "failed to persist ${forConfigObject.javaClass.simpleName}", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun conversationInConfig(
|
||||
publicKey: String?,
|
||||
groupPublicKey: String?,
|
||||
openGroupId: String?,
|
||||
visibleOnly: Boolean
|
||||
): Boolean {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return true
|
||||
|
||||
val (_, userPublicKey) = maybeGetUserInfo() ?: return true
|
||||
|
||||
if (openGroupId != null) {
|
||||
val userGroups = userGroups ?: return false
|
||||
val threadId = GroupManager.getOpenGroupThreadID(openGroupId, context)
|
||||
val openGroup = get(context).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return false
|
||||
|
||||
// Not handling the `hidden` behaviour for communities so just indicate the existence
|
||||
return (userGroups.getCommunityInfo(openGroup.server, openGroup.room) != null)
|
||||
}
|
||||
else if (groupPublicKey != null) {
|
||||
val userGroups = userGroups ?: return false
|
||||
|
||||
// Not handling the `hidden` behaviour for legacy groups so just indicate the existence
|
||||
return (userGroups.getLegacyGroupInfo(groupPublicKey) != null)
|
||||
}
|
||||
else if (publicKey == userPublicKey) {
|
||||
val user = user ?: return false
|
||||
|
||||
return (!visibleOnly || user.getNtsPriority() != ConfigBase.PRIORITY_HIDDEN)
|
||||
}
|
||||
else if (publicKey != null) {
|
||||
val contacts = contacts ?: return false
|
||||
val targetContact = contacts.get(publicKey) ?: return false
|
||||
|
||||
return (!visibleOnly || targetContact.priority != ConfigBase.PRIORITY_HIDDEN)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun canPerformChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean {
|
||||
if (!ConfigBase.isNewConfigEnabled(isConfigForcedOn, SnodeAPI.nowWithOffset)) return true
|
||||
|
||||
val lastUpdateTimestampMs = configDatabase.retrieveConfigLastUpdateTimestamp(variant, publicKey)
|
||||
|
||||
// Ensure the change occurred after the last config message was handled (minus the buffer period)
|
||||
return (changeTimestampMs >= (lastUpdateTimestampMs - ConfigFactory.configChangeBufferPeriod))
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
package org.thoughtcrime.securesms.dependencies;
|
||||
|
||||
public interface InjectableType {
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.session.libsession.utilities.ConfigFactoryUpdateListener
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import org.thoughtcrime.securesms.database.ConfigDatabase
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object SessionUtilModule {
|
||||
|
||||
private fun maybeUserEdSecretKey(context: Context): ByteArray? {
|
||||
val edKey = KeyPairUtilities.getUserED25519KeyPair(context) ?: return null
|
||||
return edKey.secretKey.asBytes
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConfigFactory(@ApplicationContext context: Context, configDatabase: ConfigDatabase): ConfigFactory =
|
||||
ConfigFactory(context, configDatabase) {
|
||||
val localUserPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val secretKey = maybeUserEdSecretKey(context)
|
||||
if (localUserPublicKey == null || secretKey == null) null
|
||||
else secretKey to localUserPublicKey
|
||||
}.apply {
|
||||
registerListener(context as ConfigFactoryUpdateListener)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.content.Context
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
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.GroupRecord
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
|
||||
object ClosedGroupManager {
|
||||
|
||||
fun silentlyRemoveGroup(context: Context, threadId: Long, groupPublicKey: String, groupID: String, userPublicKey: String, delete: Boolean = true) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
// Mark the group as inactive
|
||||
storage.setActive(groupID, false)
|
||||
storage.removeClosedGroupPublicKey(groupPublicKey)
|
||||
// Remove the key pairs
|
||||
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
// Stop polling
|
||||
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
||||
storage.cancelPendingMessageSendJobs(threadId)
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||
if (delete) {
|
||||
storage.deleteConversation(threadId)
|
||||
}
|
||||
}
|
||||
|
||||
fun ConfigFactory.removeLegacyGroup(group: GroupRecord): Boolean {
|
||||
val groups = userGroups ?: return false
|
||||
if (!group.isClosedGroup) return false
|
||||
val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId())
|
||||
return groups.eraseLegacyGroup(groupPublicKey)
|
||||
}
|
||||
|
||||
fun ConfigFactory.updateLegacyGroup(groupRecipientSettings: Recipient.RecipientSettings, group: GroupRecord) {
|
||||
val groups = userGroups ?: return
|
||||
if (!group.isClosedGroup) return
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val threadId = storage.getThreadId(group.encodedId) ?: return
|
||||
val groupPublicKey = GroupUtil.doubleEncodeGroupID(group.getId())
|
||||
val latestKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||
val legacyInfo = groups.getOrConstructLegacyGroupInfo(groupPublicKey)
|
||||
val latestMemberMap = GroupUtil.createConfigMemberMap(group.members.map(Address::serialize), group.admins.map(Address::serialize))
|
||||
val toSet = legacyInfo.copy(
|
||||
members = latestMemberMap,
|
||||
name = group.title,
|
||||
disappearingTimer = groupRecipientSettings.expireMessages.toLong(),
|
||||
priority = if (storage.isPinned(threadId)) ConfigBase.PRIORITY_PINNED else ConfigBase.PRIORITY_VISIBLE,
|
||||
encPubKey = (latestKeyPair.publicKey as DjbECPublicKey).publicKey, // 'serialize()' inserts an extra byte
|
||||
encSecKey = latestKeyPair.privateKey.serialize()
|
||||
)
|
||||
groups.set(toSet)
|
||||
}
|
||||
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.res.TypedArrayUtils;
|
||||
import androidx.preference.DialogPreference;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.takisoft.colorpicker.ColorPickerDialog;
|
||||
import com.takisoft.colorpicker.ColorPickerDialog.Size;
|
||||
import com.takisoft.colorpicker.ColorStateDrawable;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class ColorPickerPreference extends DialogPreference {
|
||||
|
||||
private static final String TAG = ColorPickerPreference.class.getSimpleName();
|
||||
|
||||
private int[] colors;
|
||||
private CharSequence[] colorDescriptions;
|
||||
private int color;
|
||||
private int columns;
|
||||
private int size;
|
||||
private boolean sortColors;
|
||||
|
||||
private ImageView colorWidget;
|
||||
private OnPreferenceChangeListener listener;
|
||||
|
||||
public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerPreference, defStyleAttr, 0);
|
||||
|
||||
int colorsId = a.getResourceId(R.styleable.ColorPickerPreference_colors, R.array.color_picker_default_colors);
|
||||
|
||||
if (colorsId != 0) {
|
||||
colors = context.getResources().getIntArray(colorsId);
|
||||
}
|
||||
|
||||
colorDescriptions = a.getTextArray(R.styleable.ColorPickerPreference_colorDescriptions);
|
||||
color = a.getColor(R.styleable.ColorPickerPreference_currentColor, 0);
|
||||
columns = a.getInt(R.styleable.ColorPickerPreference_columns, 3);
|
||||
size = a.getInt(R.styleable.ColorPickerPreference_colorSize, 2);
|
||||
sortColors = a.getBoolean(R.styleable.ColorPickerPreference_sortColors, false);
|
||||
|
||||
a.recycle();
|
||||
|
||||
setWidgetLayoutResource(R.layout.preference_widget_color_swatch);
|
||||
}
|
||||
|
||||
public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
public ColorPickerPreference(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.dialogPreferenceStyle,
|
||||
android.R.attr.dialogPreferenceStyle));
|
||||
}
|
||||
|
||||
public ColorPickerPreference(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnPreferenceChangeListener(OnPreferenceChangeListener listener) {
|
||||
super.setOnPreferenceChangeListener(listener);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
colorWidget = (ImageView) holder.findViewById(R.id.color_picker_widget);
|
||||
setColorOnWidget(color);
|
||||
}
|
||||
|
||||
private void setColorOnWidget(int color) {
|
||||
if (colorWidget == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable[] colorDrawable = new Drawable[]
|
||||
{ContextCompat.getDrawable(getContext(), R.drawable.colorpickerpreference_pref_swatch)};
|
||||
colorWidget.setImageDrawable(new ColorStateDrawable(colorDrawable, color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current color.
|
||||
*
|
||||
* @return The current color.
|
||||
*/
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current color.
|
||||
*
|
||||
* @param color The current color.
|
||||
*/
|
||||
public void setColor(int color) {
|
||||
setInternalColor(color, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the available colors.
|
||||
*
|
||||
* @return The available colors.
|
||||
*/
|
||||
public int[] getColors() {
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the available colors.
|
||||
*
|
||||
* @param colors The available colors.
|
||||
*/
|
||||
public void setColors(int[] colors) {
|
||||
this.colors = colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the available colors should be sorted automatically based on their HSV
|
||||
* values.
|
||||
*
|
||||
* @return Whether the available colors should be sorted automatically based on their HSV
|
||||
* values.
|
||||
*/
|
||||
public boolean isSortColors() {
|
||||
return sortColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the available colors should be sorted automatically based on their HSV
|
||||
* values. The sorting does not modify the order of the original colors supplied via
|
||||
* {@link #setColors(int[])} or the XML attribute {@code app:colors}.
|
||||
*
|
||||
* @param sortColors Whether the available colors should be sorted automatically based on their
|
||||
* HSV values.
|
||||
*/
|
||||
public void setSortColors(boolean sortColors) {
|
||||
this.sortColors = sortColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available colors' descriptions that can be used by accessibility services.
|
||||
*
|
||||
* @return The available colors' descriptions.
|
||||
*/
|
||||
public CharSequence[] getColorDescriptions() {
|
||||
return colorDescriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the available colors' descriptions that can be used by accessibility services.
|
||||
*
|
||||
* @param colorDescriptions The available colors' descriptions.
|
||||
*/
|
||||
public void setColorDescriptions(CharSequence[] colorDescriptions) {
|
||||
this.colorDescriptions = colorDescriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of columns to be used in the picker dialog for displaying the available
|
||||
* colors. If the value is less than or equals to 0, the number of columns will be determined
|
||||
* automatically by the system using FlexboxLayoutManager.
|
||||
*
|
||||
* @return The number of columns to be used in the picker dialog.
|
||||
* @see com.google.android.flexbox.FlexboxLayoutManager
|
||||
*/
|
||||
public int getColumns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of columns to be used in the picker dialog for displaying the available
|
||||
* colors. If the value is less than or equals to 0, the number of columns will be determined
|
||||
* automatically by the system using FlexboxLayoutManager.
|
||||
*
|
||||
* @param columns The number of columns to be used in the picker dialog. Use 0 to set it to
|
||||
* 'auto' mode.
|
||||
* @see com.google.android.flexbox.FlexboxLayoutManager
|
||||
*/
|
||||
public void setColumns(int columns) {
|
||||
this.columns = columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the color swatches in the dialog. It can be either
|
||||
* {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}.
|
||||
*
|
||||
* @return The size of the color swatches in the dialog.
|
||||
* @see ColorPickerDialog#SIZE_SMALL
|
||||
* @see ColorPickerDialog#SIZE_LARGE
|
||||
*/
|
||||
@Size
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the color swatches in the dialog. It can be either
|
||||
* {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}.
|
||||
*
|
||||
* @param size The size of the color swatches in the dialog. It can be either
|
||||
* {@link ColorPickerDialog#SIZE_SMALL} or {@link ColorPickerDialog#SIZE_LARGE}.
|
||||
* @see ColorPickerDialog#SIZE_SMALL
|
||||
* @see ColorPickerDialog#SIZE_LARGE
|
||||
*/
|
||||
public void setSize(@Size int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
private void setInternalColor(int color, boolean force) {
|
||||
int oldColor = getPersistedInt(0);
|
||||
|
||||
boolean changed = oldColor != color;
|
||||
|
||||
if (changed || force) {
|
||||
this.color = color;
|
||||
|
||||
persistInt(color);
|
||||
|
||||
setColorOnWidget(color);
|
||||
|
||||
if (listener != null) listener.onPreferenceChange(this, color);
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object onGetDefaultValue(TypedArray a, int index) {
|
||||
return a.getString(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetInitialValue(boolean restoreValue, Object defaultValueObj) {
|
||||
final String defaultValue = (String) defaultValueObj;
|
||||
setInternalColor(restoreValue ? getPersistedInt(0) : (!TextUtils.isEmpty(defaultValue) ? Color.parseColor(defaultValue) : 0), true);
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import com.takisoft.colorpicker.ColorPickerDialog;
|
||||
import com.takisoft.colorpicker.OnColorSelectedListener;
|
||||
|
||||
public class ColorPickerPreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat implements OnColorSelectedListener {
|
||||
|
||||
private int pickedColor;
|
||||
|
||||
public static ColorPickerPreferenceDialogFragmentCompat newInstance(String key) {
|
||||
ColorPickerPreferenceDialogFragmentCompat fragment = new ColorPickerPreferenceDialogFragmentCompat();
|
||||
Bundle b = new Bundle(1);
|
||||
b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key);
|
||||
fragment.setArguments(b);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
ColorPickerPreference pref = getColorPickerPreference();
|
||||
|
||||
ColorPickerDialog.Params params = new ColorPickerDialog.Params.Builder(getContext())
|
||||
.setSelectedColor(pref.getColor())
|
||||
.setColors(pref.getColors())
|
||||
.setColorContentDescriptions(pref.getColorDescriptions())
|
||||
.setSize(pref.getSize())
|
||||
.setSortColors(pref.isSortColors())
|
||||
.setColumns(pref.getColumns())
|
||||
.build();
|
||||
|
||||
ColorPickerDialog dialog = new ColorPickerDialog(getActivity(), this, params);
|
||||
dialog.setTitle(pref.getDialogTitle());
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogClosed(boolean positiveResult) {
|
||||
ColorPickerPreference preference = getColorPickerPreference();
|
||||
|
||||
if (positiveResult) {
|
||||
preference.setColor(pickedColor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorSelected(int color) {
|
||||
this.pickedColor = color;
|
||||
|
||||
super.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
|
||||
}
|
||||
|
||||
ColorPickerPreference getColorPickerPreference() {
|
||||
return (ColorPickerPreference) getPreference();
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import network.loki.messenger.libsession_util.ConversationVolatileConfig
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
|
||||
fun ConversationVolatileConfig.getConversationUnread(thread: ThreadRecord): Boolean {
|
||||
val recipient = thread.recipient
|
||||
if (recipient.isContactRecipient
|
||||
&& recipient.isOpenGroupInboxRecipient
|
||||
&& recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) {
|
||||
return getOneToOne(recipient.address.serialize())?.unread == true
|
||||
} else if (recipient.isClosedGroupRecipient) {
|
||||
return getLegacyClosedGroup(GroupUtil.doubleDecodeGroupId(recipient.address.toGroupString()))?.unread == true
|
||||
} else if (recipient.isOpenGroupRecipient) {
|
||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(thread.threadId) ?: return false
|
||||
return getCommunity(openGroup.server, openGroup.room)?.unread == true
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="false" android:color="@color/grey"/>
|
||||
<item android:state_enabled="false" android:color="@color/gray50"/>
|
||||
<item android:color="?prominentButtonColor"/>
|
||||
</selector>
|
@ -1,26 +1,38 @@
|
||||
android.useAndroidX=true
|
||||
## For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
#
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
#
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
#Mon Jun 26 09:56:43 AEST 2023
|
||||
android.enableJetifier=true
|
||||
org.gradle.jvmargs=-Xmx8g
|
||||
|
||||
gradlePluginVersion=7.3.1
|
||||
googleServicesVersion=4.3.12
|
||||
kotlinVersion=1.6.21
|
||||
android.useAndroidX=true
|
||||
appcompatVersion=1.6.1
|
||||
coreVersion=1.8.0
|
||||
coroutinesVersion=1.6.4
|
||||
kotlinxJsonVersion=1.3.3
|
||||
lifecycleVersion=2.5.1
|
||||
curve25519Version=0.6.0
|
||||
daggerVersion=2.40.1
|
||||
glideVersion=4.11.0
|
||||
googleServicesVersion=4.3.12
|
||||
gradlePluginVersion=7.3.1
|
||||
jacksonDatabindVersion=2.9.8
|
||||
junitVersion=4.13.2
|
||||
kotlinVersion=1.6.21
|
||||
kotlinxJsonVersion=1.3.3
|
||||
kovenantVersion=3.3.0
|
||||
curve25519Version=0.6.0
|
||||
protobufVersion=2.5.0
|
||||
lifecycleVersion=2.5.1
|
||||
materialVersion=1.8.0
|
||||
mockitoKotlinVersion=4.1.0
|
||||
okhttpVersion=3.12.1
|
||||
jacksonDatabindVersion=2.9.8
|
||||
appcompatVersion=1.5.1
|
||||
materialVersion=1.7.0
|
||||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||
org.gradle.unsafe.configuration-cache=true
|
||||
pagingVersion=3.0.0
|
||||
preferenceVersion=1.2.0
|
||||
coreVersion=1.8.0
|
||||
|
||||
junitVersion=4.13.2
|
||||
mockitoKotlinVersion=4.0.0
|
||||
testCoreVersion=1.4.0
|
||||
pagingVersion=3.0.0
|
||||
protobufVersion=2.5.0
|
||||
testCoreVersion=1.5.0
|
||||
|
@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.cxx/
|
@ -0,0 +1,47 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'network.loki.messenger.libsession_util'
|
||||
compileSdkVersion androidCompileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion androidMinimumSdkVersion
|
||||
targetSdkVersion androidCompileSdkVersion
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/cpp/CMakeLists.txt"
|
||||
version "3.22.1"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
implementation(project(":libsignal"))
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
|
||||
}
|
@ -0,0 +1 @@
|
||||
Subproject commit 7eb87028355bfc89950102c52d5b2927a25b2e22
|
@ -0,0 +1,584 @@
|
||||
package network.loki.messenger.libsession_util
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import network.loki.messenger.libsession_util.util.*
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class InstrumentedTests {
|
||||
|
||||
val seed =
|
||||
Hex.fromStringCondensed("0123456789abcdef0123456789abcdef00000000000000000000000000000000")
|
||||
|
||||
private val keyPair: KeyPair
|
||||
get() {
|
||||
return Sodium.ed25519KeyPair(seed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("network.loki.messenger.libsession_util.test", appContext.packageName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_test_sodium_kp_ed_curve() {
|
||||
val kp = keyPair
|
||||
val curvePkBytes = Sodium.ed25519PkToCurve25519(kp.pubKey)
|
||||
|
||||
val edPk = kp.pubKey
|
||||
val curvePk = curvePkBytes
|
||||
|
||||
assertArrayEquals(Hex.fromStringCondensed("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"), edPk)
|
||||
assertArrayEquals(Hex.fromStringCondensed("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"), curvePk)
|
||||
assertArrayEquals(kp.secretKey.take(32).toByteArray(), seed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDirtyEmptyString() {
|
||||
val contacts = Contacts.newInstance(keyPair.secretKey)
|
||||
val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000"
|
||||
val contact = contacts.getOrConstruct(definitelyRealId)
|
||||
contacts.set(contact)
|
||||
assertTrue(contacts.dirty())
|
||||
contacts.set(contact.copy(name = "test"))
|
||||
assertTrue(contacts.dirty())
|
||||
val push = contacts.push()
|
||||
contacts.confirmPushed(push.seqNo, "abc123")
|
||||
contacts.dump()
|
||||
contacts.set(contact.copy(name = "test2"))
|
||||
contacts.set(contact.copy(name = "test"))
|
||||
assertTrue(contacts.dirty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_contacts() {
|
||||
val contacts = Contacts.newInstance(keyPair.secretKey)
|
||||
val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000"
|
||||
assertNull(contacts.get(definitelyRealId))
|
||||
|
||||
// Should be an uninitialized contact apart from ID
|
||||
val c = contacts.getOrConstruct(definitelyRealId)
|
||||
assertEquals(definitelyRealId, c.id)
|
||||
assertTrue(c.name.isEmpty())
|
||||
assertTrue(c.nickname.isEmpty())
|
||||
assertFalse(c.approved)
|
||||
assertFalse(c.approvedMe)
|
||||
assertFalse(c.blocked)
|
||||
assertEquals(UserPic.DEFAULT, c.profilePicture)
|
||||
|
||||
assertFalse(contacts.needsPush())
|
||||
assertFalse(contacts.needsDump())
|
||||
assertEquals(0, contacts.push().seqNo)
|
||||
|
||||
c.name = "Joe"
|
||||
c.nickname = "Joey"
|
||||
c.approved = true
|
||||
c.approvedMe = true
|
||||
|
||||
contacts.set(c)
|
||||
|
||||
val cSaved = contacts.get(definitelyRealId)!!
|
||||
assertEquals("Joe", cSaved.name)
|
||||
assertEquals("Joey", cSaved.nickname)
|
||||
assertTrue(cSaved.approved)
|
||||
assertTrue(cSaved.approvedMe)
|
||||
assertFalse(cSaved.blocked)
|
||||
assertEquals(UserPic.DEFAULT, cSaved.profilePicture)
|
||||
|
||||
val push1 = contacts.push()
|
||||
|
||||
assertEquals(1, push1.seqNo)
|
||||
contacts.confirmPushed(push1.seqNo, "fakehash1")
|
||||
assertFalse(contacts.needsPush())
|
||||
assertTrue(contacts.needsDump())
|
||||
|
||||
val contacts2 = Contacts.newInstance(keyPair.secretKey, contacts.dump())
|
||||
assertFalse(contacts.needsDump())
|
||||
assertFalse(contacts2.needsPush())
|
||||
assertFalse(contacts2.needsDump())
|
||||
|
||||
val anotherId = "051111111111111111111111111111111111111111111111111111111111111111"
|
||||
val c2 = contacts2.getOrConstruct(anotherId)
|
||||
contacts2.set(c2)
|
||||
val push2 = contacts2.push()
|
||||
assertEquals(2, push2.seqNo)
|
||||
contacts2.confirmPushed(push2.seqNo, "fakehash2")
|
||||
assertFalse(contacts2.needsPush())
|
||||
|
||||
contacts.merge("fakehash2" to push2.config)
|
||||
|
||||
|
||||
assertFalse(contacts.needsPush())
|
||||
assertEquals(push2.seqNo, contacts.push().seqNo)
|
||||
|
||||
val contactList = contacts.all().toList()
|
||||
assertEquals(definitelyRealId, contactList[0].id)
|
||||
assertEquals(anotherId, contactList[1].id)
|
||||
assertEquals("Joey", contactList[0].nickname)
|
||||
assertEquals("", contactList[1].nickname)
|
||||
|
||||
contacts.erase(definitelyRealId)
|
||||
|
||||
val thirdId ="052222222222222222222222222222222222222222222222222222222222222222"
|
||||
val third = Contact(
|
||||
id = thirdId,
|
||||
nickname = "Nickname 3",
|
||||
approved = true,
|
||||
blocked = true,
|
||||
profilePicture = UserPic("http://example.com/huge.bmp", "qwertyuio01234567890123456789012".encodeToByteArray()),
|
||||
expiryMode = ExpiryMode.NONE
|
||||
)
|
||||
contacts2.set(third)
|
||||
assertTrue(contacts.needsPush())
|
||||
assertTrue(contacts2.needsPush())
|
||||
val toPush = contacts.push()
|
||||
val toPush2 = contacts2.push()
|
||||
assertEquals(toPush.seqNo, toPush2.seqNo)
|
||||
assertThat(toPush2.config, not(equals(toPush.config)))
|
||||
|
||||
contacts.confirmPushed(toPush.seqNo, "fakehash3a")
|
||||
contacts2.confirmPushed(toPush2.seqNo, "fakehash3b")
|
||||
|
||||
contacts.merge("fakehash3b" to toPush2.config)
|
||||
contacts2.merge("fakehash3a" to toPush.config)
|
||||
|
||||
assertTrue(contacts.needsPush())
|
||||
assertTrue(contacts2.needsPush())
|
||||
|
||||
val mergePush = contacts.push()
|
||||
val mergePush2 = contacts2.push()
|
||||
|
||||
assertEquals(mergePush.seqNo, mergePush2.seqNo)
|
||||
assertArrayEquals(mergePush.config, mergePush2.config)
|
||||
|
||||
assertTrue(mergePush.obsoleteHashes.containsAll(listOf("fakehash3b", "fakehash3a")))
|
||||
assertTrue(mergePush2.obsoleteHashes.containsAll(listOf("fakehash3b", "fakehash3a")))
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_accessible() {
|
||||
val userProfile = UserProfile.newInstance(keyPair.secretKey)
|
||||
assertNotNull(userProfile)
|
||||
userProfile.free()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_user_profile_c_api() {
|
||||
val edSk = keyPair.secretKey
|
||||
val userProfile = UserProfile.newInstance(edSk)
|
||||
|
||||
// these should be false as empty config
|
||||
assertFalse(userProfile.needsPush())
|
||||
assertFalse(userProfile.needsDump())
|
||||
|
||||
// Since it's empty there shouldn't be a name
|
||||
assertNull(userProfile.getName())
|
||||
|
||||
// Don't need to push yet so this is just for testing
|
||||
val (_, seqNo) = userProfile.push() // disregarding encrypted
|
||||
assertEquals("UserProfile", userProfile.encryptionDomain())
|
||||
assertEquals(0, seqNo)
|
||||
|
||||
// This should also be unset:
|
||||
assertEquals(UserPic.DEFAULT, userProfile.getPic())
|
||||
|
||||
// Now let's go set a profile name and picture:
|
||||
// not sending keylen like c api so cutting off the NOTSECRET in key for testing purposes
|
||||
userProfile.setName("Kallie")
|
||||
val newUserPic = UserPic("http://example.org/omg-pic-123.bmp", "secret78901234567890123456789012".encodeToByteArray())
|
||||
userProfile.setPic(newUserPic)
|
||||
userProfile.setNtsPriority(9)
|
||||
|
||||
// Retrieve them just to make sure they set properly:
|
||||
assertEquals("Kallie", userProfile.getName())
|
||||
val pic = userProfile.getPic()
|
||||
assertEquals("http://example.org/omg-pic-123.bmp", pic.url)
|
||||
assertEquals("secret78901234567890123456789012", pic.key.decodeToString())
|
||||
|
||||
// Since we've made changes, we should need to push new config to the swarm, *and* should need
|
||||
// to dump the updated state:
|
||||
assertTrue(userProfile.needsPush())
|
||||
assertTrue(userProfile.needsDump())
|
||||
val (newToPush, newSeqNo) = userProfile.push()
|
||||
|
||||
val expHash0 =
|
||||
Hex.fromStringCondensed("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
|
||||
|
||||
val expectedPush1Decrypted = ("d" +
|
||||
"1:#"+ "i1e" +
|
||||
"1:&"+ "d"+
|
||||
"1:+"+ "i9e"+
|
||||
"1:n"+ "6:Kallie"+
|
||||
"1:p"+ "34:http://example.org/omg-pic-123.bmp"+
|
||||
"1:q"+ "32:secret78901234567890123456789012"+
|
||||
"e"+
|
||||
"1:<"+ "l"+
|
||||
"l"+ "i0e"+ "32:").encodeToByteArray() + expHash0 + ("de"+ "e"+
|
||||
"e"+
|
||||
"1:="+ "d"+
|
||||
"1:+" +"0:"+
|
||||
"1:n" +"0:"+
|
||||
"1:p" +"0:"+
|
||||
"1:q" +"0:"+
|
||||
"e"+
|
||||
"e").encodeToByteArray()
|
||||
|
||||
assertEquals(1, newSeqNo)
|
||||
// We haven't dumped, so still need to dump:
|
||||
assertTrue(userProfile.needsDump())
|
||||
// We did call push but we haven't confirmed it as stored yet, so this will still return true:
|
||||
assertTrue(userProfile.needsPush())
|
||||
|
||||
val dump = userProfile.dump()
|
||||
// (in a real client we'd now store this to disk)
|
||||
assertFalse(userProfile.needsDump())
|
||||
val expectedDump = ("d" +
|
||||
"1:!"+ "i2e" +
|
||||
"1:$").encodeToByteArray() + expectedPush1Decrypted.size.toString().encodeToByteArray() +
|
||||
":".encodeToByteArray() + expectedPush1Decrypted +
|
||||
"1:(0:1:)le".encodeToByteArray()+
|
||||
"e".encodeToByteArray()
|
||||
|
||||
assertArrayEquals(expectedDump, dump)
|
||||
|
||||
userProfile.confirmPushed(newSeqNo, "fakehash1")
|
||||
|
||||
val newConf = UserProfile.newInstance(edSk)
|
||||
|
||||
val accepted = newConf.merge("fakehash1" to newToPush)
|
||||
assertEquals(1, accepted)
|
||||
|
||||
assertTrue(newConf.needsDump())
|
||||
assertFalse(newConf.needsPush())
|
||||
val _ignore = newConf.dump()
|
||||
assertFalse(newConf.needsDump())
|
||||
|
||||
|
||||
userProfile.setName("Raz")
|
||||
newConf.setName("Nibbler")
|
||||
newConf.setPic(UserPic("http://new.example.com/pic", "qwertyuio01234567890123456789012".encodeToByteArray()))
|
||||
|
||||
val conf = userProfile.push()
|
||||
val conf2 = newConf.push()
|
||||
|
||||
userProfile.confirmPushed(conf.seqNo, "fakehash2")
|
||||
newConf.confirmPushed(conf2.seqNo, "fakehash3")
|
||||
|
||||
userProfile.dump()
|
||||
|
||||
assertFalse(conf.config.contentEquals(conf2.config))
|
||||
|
||||
newConf.merge("fakehash2" to conf.config)
|
||||
userProfile.merge("fakehash3" to conf2.config)
|
||||
|
||||
assertTrue(newConf.needsPush())
|
||||
assertTrue(userProfile.needsPush())
|
||||
|
||||
val newSeq1 = userProfile.push()
|
||||
|
||||
assertEquals(3, newSeq1.seqNo)
|
||||
|
||||
userProfile.confirmPushed(newSeq1.seqNo, "fakehash4")
|
||||
|
||||
// assume newConf push gets rejected as it was last to write and clear previous config by hash on oxenss
|
||||
newConf.merge("fakehash4" to newSeq1.config)
|
||||
|
||||
val newSeqMerge = newConf.push()
|
||||
|
||||
newConf.confirmPushed(newSeqMerge.seqNo, "fakehash5")
|
||||
|
||||
assertEquals("Raz", newConf.getName())
|
||||
assertEquals(3, newSeqMerge.seqNo)
|
||||
|
||||
// userProfile device polls and merges
|
||||
userProfile.merge("fakehash5" to newSeqMerge.config)
|
||||
|
||||
val userConfigMerge = userProfile.push()
|
||||
|
||||
assertEquals(3, userConfigMerge.seqNo)
|
||||
|
||||
assertEquals("Raz", newConf.getName())
|
||||
assertEquals("Raz", userProfile.getName())
|
||||
|
||||
userProfile.free()
|
||||
newConf.free()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun merge_resolves_conflicts() {
|
||||
val kp = keyPair
|
||||
val a = UserProfile.newInstance(kp.secretKey)
|
||||
val b = UserProfile.newInstance(kp.secretKey)
|
||||
a.setName("A")
|
||||
val (aPush, aSeq) = a.push()
|
||||
a.confirmPushed(aSeq, "hashfroma")
|
||||
b.setName("B")
|
||||
// polls and sees invalid state, has to merge
|
||||
b.merge("hashfroma" to aPush)
|
||||
val (bPush, bSeq) = b.push()
|
||||
b.confirmPushed(bSeq, "hashfromb")
|
||||
assertEquals("B", b.getName())
|
||||
assertEquals(1, aSeq)
|
||||
assertEquals(2, bSeq)
|
||||
a.merge("hashfromb" to bPush)
|
||||
assertEquals(2, a.push().seqNo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_setting_getting() {
|
||||
val userProfile = UserProfile.newInstance(keyPair.secretKey)
|
||||
val newName = "test"
|
||||
println("Name being set via JNI call: $newName")
|
||||
userProfile.setName(newName)
|
||||
val nameFromNative = userProfile.getName()
|
||||
assertEquals(newName, nameFromNative)
|
||||
println("Name received by JNI call: $nameFromNative")
|
||||
assertTrue(userProfile.dirty())
|
||||
userProfile.free()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jni_remove_all_test() {
|
||||
val convos = ConversationVolatileConfig.newInstance(keyPair.secretKey)
|
||||
assertEquals(0 /* number removed */, convos.eraseAll { true /* 'erase' every item */ })
|
||||
|
||||
val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000"
|
||||
val definitelyRealConvo = Conversation.OneToOne(definitelyRealId, System.currentTimeMillis(), false)
|
||||
convos.set(definitelyRealConvo)
|
||||
|
||||
val anotherDefinitelyReadId = "051111111111111111111111111111111111111111111111111111111111111111"
|
||||
val anotherDefinitelyRealConvo = Conversation.OneToOne(anotherDefinitelyReadId, System.currentTimeMillis(), false)
|
||||
convos.set(anotherDefinitelyRealConvo)
|
||||
|
||||
assertEquals(2, convos.sizeOneToOnes())
|
||||
|
||||
val numErased = convos.eraseAll { convo ->
|
||||
convo is Conversation.OneToOne && convo.sessionId == definitelyRealId
|
||||
}
|
||||
assertEquals(1, numErased)
|
||||
assertEquals(1, convos.sizeOneToOnes())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_open_group_urls() {
|
||||
val (base1, room1, pk1) = BaseCommunityInfo.parseFullUrl(
|
||||
"https://example.com/" +
|
||||
"someroom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
)!!
|
||||
|
||||
val (base2, room2, pk2) = BaseCommunityInfo.parseFullUrl(
|
||||
"HTTPS://EXAMPLE.COM/" +
|
||||
"someroom?public_key=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base3, room3, pk3) = BaseCommunityInfo.parseFullUrl(
|
||||
"HTTPS://EXAMPLE.COM/r/" +
|
||||
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base4, room4, pk4) = BaseCommunityInfo.parseFullUrl(
|
||||
"http://example.com/r/" +
|
||||
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base5, room5, pk5) = BaseCommunityInfo.parseFullUrl(
|
||||
"HTTPS://EXAMPLE.com:443/r/" +
|
||||
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base6, room6, pk6) = BaseCommunityInfo.parseFullUrl(
|
||||
"HTTP://EXAMPLE.com:80/r/" +
|
||||
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
|
||||
)!!
|
||||
|
||||
val (base7, room7, pk7) = BaseCommunityInfo.parseFullUrl(
|
||||
"http://example.com:80/r/" +
|
||||
"someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8"
|
||||
)!!
|
||||
val (base8, room8, pk8) = BaseCommunityInfo.parseFullUrl(
|
||||
"http://example.com:80/r/" +
|
||||
"someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo"
|
||||
)!!
|
||||
|
||||
assertEquals("https://example.com", base1)
|
||||
assertEquals("http://example.com", base4)
|
||||
assertEquals(base1, base2)
|
||||
assertEquals(base1, base3)
|
||||
assertNotEquals(base1, base4)
|
||||
assertEquals(base1, base5)
|
||||
assertEquals(base4, base6)
|
||||
assertEquals(base4, base7)
|
||||
assertEquals(base4, base8)
|
||||
assertEquals("someroom", room1)
|
||||
assertEquals("someroom", room2)
|
||||
assertEquals("someroom", room3)
|
||||
assertEquals("someroom", room4)
|
||||
assertEquals("someroom", room5)
|
||||
assertEquals("someroom", room6)
|
||||
assertEquals("someroom", room7)
|
||||
assertEquals("someroom", room8)
|
||||
assertEquals(Hex.toStringCondensed(pk1), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk2), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk3), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk4), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk5), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk6), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk7), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
assertEquals(Hex.toStringCondensed(pk8), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_conversations() {
|
||||
val convos = ConversationVolatileConfig.newInstance(keyPair.secretKey)
|
||||
val definitelyRealId = "055000000000000000000000000000000000000000000000000000000000000000"
|
||||
assertNull(convos.getOneToOne(definitelyRealId))
|
||||
assertTrue(convos.empty())
|
||||
assertEquals(0, convos.size())
|
||||
|
||||
val c = convos.getOrConstructOneToOne(definitelyRealId)
|
||||
|
||||
assertEquals(definitelyRealId, c.sessionId)
|
||||
assertEquals(0, c.lastRead)
|
||||
|
||||
assertFalse(convos.needsPush())
|
||||
assertFalse(convos.needsDump())
|
||||
assertEquals(0, convos.push().seqNo)
|
||||
|
||||
val nowMs = System.currentTimeMillis()
|
||||
|
||||
c.lastRead = nowMs
|
||||
|
||||
convos.set(c)
|
||||
|
||||
assertNull(convos.getLegacyClosedGroup(definitelyRealId))
|
||||
assertNotNull(convos.getOneToOne(definitelyRealId))
|
||||
assertEquals(nowMs, convos.getOneToOne(definitelyRealId)?.lastRead)
|
||||
|
||||
assertTrue(convos.needsPush())
|
||||
assertTrue(convos.needsDump())
|
||||
|
||||
val openGroupPubKey = Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
|
||||
|
||||
val og = convos.getOrConstructCommunity("http://Example.ORG:5678", "SudokuRoom", openGroupPubKey)
|
||||
val ogCommunity = og.baseCommunityInfo
|
||||
|
||||
assertEquals("http://example.org:5678", ogCommunity.baseUrl) // Note: lower-case
|
||||
assertEquals("sudokuroom", ogCommunity.room) // Note: lower-case
|
||||
assertEquals(64, ogCommunity.pubKeyHex.length)
|
||||
assertEquals("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ogCommunity.pubKeyHex)
|
||||
|
||||
og.unread = true
|
||||
|
||||
convos.set(og)
|
||||
|
||||
val (_, seqNo) = convos.push()
|
||||
|
||||
assertEquals(1, seqNo)
|
||||
|
||||
convos.confirmPushed(seqNo, "fakehash1")
|
||||
|
||||
assertTrue(convos.needsDump())
|
||||
assertFalse(convos.needsPush())
|
||||
|
||||
val convos2 = ConversationVolatileConfig.newInstance(keyPair.secretKey, convos.dump())
|
||||
assertFalse(convos.needsPush())
|
||||
assertFalse(convos.needsDump())
|
||||
assertEquals(1, convos.push().seqNo)
|
||||
assertFalse(convos.needsDump())
|
||||
|
||||
val x1 = convos2.getOneToOne(definitelyRealId)!!
|
||||
assertEquals(nowMs, x1.lastRead)
|
||||
assertEquals(definitelyRealId, x1.sessionId)
|
||||
assertEquals(false, x1.unread)
|
||||
|
||||
val x2 = convos2.getCommunity("http://EXAMPLE.org:5678", "sudokuRoom")!!
|
||||
val x2Info = x2.baseCommunityInfo
|
||||
assertEquals("http://example.org:5678", x2Info.baseUrl)
|
||||
assertEquals("sudokuroom", x2Info.room)
|
||||
assertEquals(x2Info.pubKeyHex, Hex.toStringCondensed(openGroupPubKey))
|
||||
assertTrue(x2.unread)
|
||||
|
||||
val anotherId = "051111111111111111111111111111111111111111111111111111111111111111"
|
||||
val c2 = convos.getOrConstructOneToOne(anotherId)
|
||||
c2.unread = true
|
||||
convos2.set(c2)
|
||||
|
||||
val c3 = convos.getOrConstructLegacyGroup(
|
||||
"05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||
)
|
||||
c3.lastRead = nowMs - 50
|
||||
convos2.set(c3)
|
||||
|
||||
assertTrue(convos2.needsPush())
|
||||
|
||||
val (toPush2, seqNo2) = convos2.push()
|
||||
assertEquals(2, seqNo2)
|
||||
|
||||
convos2.confirmPushed(seqNo2, "fakehash2")
|
||||
convos.merge("fakehash2" to toPush2)
|
||||
|
||||
assertFalse(convos.needsPush())
|
||||
assertEquals(seqNo2, convos.push().seqNo)
|
||||
|
||||
val seen = mutableListOf<String>()
|
||||
for ((ind, conv) in listOf(convos, convos2).withIndex()) {
|
||||
Log.e("Test","Testing seen from convo #$ind")
|
||||
seen.clear()
|
||||
assertEquals(4, conv.size())
|
||||
assertEquals(2, conv.sizeOneToOnes())
|
||||
assertEquals(1, conv.sizeCommunities())
|
||||
assertEquals(1, conv.sizeLegacyClosedGroups())
|
||||
assertFalse(conv.empty())
|
||||
val allConvos = conv.all()
|
||||
for (convo in allConvos) {
|
||||
when (convo) {
|
||||
is Conversation.OneToOne -> seen.add("1-to-1: ${convo.sessionId}")
|
||||
is Conversation.Community -> seen.add("og: ${convo.baseCommunityInfo.baseUrl}/r/${convo.baseCommunityInfo.room}")
|
||||
is Conversation.LegacyGroup -> seen.add("cl: ${convo.groupId}")
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(seen.contains("1-to-1: 051111111111111111111111111111111111111111111111111111111111111111"))
|
||||
assertTrue(seen.contains("1-to-1: 055000000000000000000000000000000000000000000000000000000000000000"))
|
||||
assertTrue(seen.contains("og: http://example.org:5678/r/sudokuroom"))
|
||||
assertTrue(seen.contains("cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"))
|
||||
assertTrue(seen.size == 4) // for some reason iterative checks aren't working in test cases
|
||||
}
|
||||
|
||||
assertFalse(convos.needsPush())
|
||||
convos.eraseOneToOne("052000000000000000000000000000000000000000000000000000000000000000")
|
||||
assertFalse(convos.needsPush())
|
||||
convos.eraseOneToOne("055000000000000000000000000000000000000000000000000000000000000000")
|
||||
assertTrue(convos.needsPush())
|
||||
|
||||
assertEquals(1, convos.allOneToOnes().size)
|
||||
assertEquals("051111111111111111111111111111111111111111111111111111111111111111",
|
||||
convos.allOneToOnes().map(Conversation.OneToOne::sessionId).first()
|
||||
)
|
||||
assertEquals(1, convos.allCommunities().size)
|
||||
assertEquals("http://example.org:5678",
|
||||
convos.allCommunities().map { it.baseCommunityInfo.baseUrl }.first()
|
||||
)
|
||||
assertEquals(1, convos.allLegacyClosedGroups().size)
|
||||
assertEquals("05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
convos.allLegacyClosedGroups().map(Conversation.LegacyGroup::groupId).first()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue