Merge remote-tracking branch 'origin/dev' into closed_groups

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
#	app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt
This commit is contained in:
SessionHero01 2024-10-23 15:16:43 +11:00
commit 7c1efeaaaa
No known key found for this signature in database
9 changed files with 196 additions and 91 deletions

View File

@ -109,6 +109,13 @@ android {
String sharedTestDir = 'src/sharedTest/java' String sharedTestDir = 'src/sharedTest/java'
test.java.srcDirs += sharedTestDir test.java.srcDirs += sharedTestDir
androidTest.java.srcDirs += sharedTestDir androidTest.java.srcDirs += sharedTestDir
main {
assets.srcDirs += "$buildDir/generated/binary"
}
test {
resources.srcDirs += "$buildDir/generated/binary"
resources.srcDirs += "$projectDir/src/main/assets"
}
} }
buildTypes { buildTypes {
@ -246,6 +253,12 @@ android {
} }
} }
apply {
from("ipToCode.gradle.kts")
}
preBuild.dependsOn ipToCode
dependencies { dependencies {
implementation project(':content-descriptions') implementation project(':content-descriptions')

41
app/ipToCode.gradle.kts Normal file
View File

@ -0,0 +1,41 @@
import java.io.File
import java.io.DataOutputStream
import java.io.FileOutputStream
task("ipToCode") {
val inputFile = File("${projectDir}/geolite2_country_blocks_ipv4.csv")
val outputDir = "${buildDir}/generated/binary"
val outputFile = File(outputDir, "geolite2_country_blocks_ipv4.bin").apply { parentFile.mkdirs() }
outputs.file(outputFile)
doLast {
// Ensure the input file exists
if (!inputFile.exists()) {
throw IllegalArgumentException("Input file does not exist: ${inputFile.absolutePath}")
}
// Create a DataOutputStream to write binary data
DataOutputStream(FileOutputStream(outputFile)).use { out ->
inputFile.useLines { lines ->
var prevCode = -1
lines.drop(1).forEach { line ->
runCatching {
val ints = line.split(".", "/", ",")
val code = ints[5].toInt().also { if (it == prevCode) return@forEach }
val ip = ints.take(4).fold(0) { acc, s -> acc shl 8 or s.toInt() }
out.writeInt(ip)
out.writeInt(code)
prevCode = code
}
}
}
}
println("Processed data written to: ${outputFile.absolutePath}")
}
}

View File

@ -36,6 +36,7 @@ class ProfilePictureView @JvmOverloads constructor(
var displayName: String? = null var displayName: String? = null
var additionalPublicKey: String? = null var additionalPublicKey: String? = null
var additionalDisplayName: String? = null var additionalDisplayName: String? = null
var recipient: Recipient? = null
private val profilePicturesCache = mutableMapOf<View, Recipient>() private val profilePicturesCache = mutableMapOf<View, Recipient>()
private val resourcePadding by lazy { private val resourcePadding by lazy {
@ -51,6 +52,7 @@ class ProfilePictureView @JvmOverloads constructor(
} }
fun update(recipient: Recipient) { fun update(recipient: Recipient) {
this.recipient = recipient
recipient.run { update(address, isLegacyClosedGroupRecipient, isOpenGroupInboxRecipient) } recipient.run { update(address, isLegacyClosedGroupRecipient, isOpenGroupInboxRecipient) }
} }
@ -121,7 +123,14 @@ class ProfilePictureView @JvmOverloads constructor(
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) { private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) {
if (publicKey.isNotEmpty()) { if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) // if we already have a recipient that matches the current key, reuse it
val recipient = if(this.recipient != null && this.recipient?.address?.serialize() == publicKey){
this.recipient!!
}
else {
this.recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
this.recipient!!
}
if (profilePicturesCache[imageView] == recipient) return if (profilePicturesCache[imageView] == recipient) return
profilePicturesCache[imageView] = recipient profilePicturesCache[imageView] = recipient
val signalProfilePicture = recipient.contactPhoto val signalProfilePicture = recipient.contactPhoto

View File

@ -173,11 +173,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
binding.run { binding.run {
profilePictureView.apply {
publicKey = viewModel.hexEncodedPublicKey
displayName = viewModel.getDisplayName()
update()
}
profilePictureView.setOnClickListener { profilePictureView.setOnClickListener {
binding.avatarDialog.isVisible = true binding.avatarDialog.isVisible = true
showAvatarDialog = true showAvatarDialog = true
@ -209,6 +204,18 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
binding.profilePictureView.update() binding.profilePictureView.update()
} }
} }
lifecycleScope.launch {
viewModel.avatarData.collect {
if(it == null) return@collect
binding.profilePictureView.apply {
publicKey = it.publicKey
displayName = it.displayName
update(it.recipient)
}
}
}
} }
override fun onStart() { override fun onStart() {

View File

@ -25,6 +25,7 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ProfileKeyUtil import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.ProfilePictureUtilities import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.truncateIdForDisplay import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -50,7 +51,7 @@ class SettingsViewModel @Inject constructor(
private var tempFile: File? = null private var tempFile: File? = null
val hexEncodedPublicKey: String get() = prefs.getLocalNumber() ?: "" val hexEncodedPublicKey: String = prefs.getLocalNumber() ?: ""
private val userAddress = Address.fromSerialized(hexEncodedPublicKey) private val userAddress = Address.fromSerialized(hexEncodedPublicKey)
@ -68,6 +69,10 @@ class SettingsViewModel @Inject constructor(
val recoveryHidden: StateFlow<Boolean> val recoveryHidden: StateFlow<Boolean>
get() = _recoveryHidden get() = _recoveryHidden
private val _avatarData: MutableStateFlow<AvatarData?> = MutableStateFlow(null)
val avatarData: StateFlow<AvatarData?>
get() = _avatarData
/** /**
* Refreshes the avatar on the main settings page * Refreshes the avatar on the main settings page
*/ */
@ -75,6 +80,19 @@ class SettingsViewModel @Inject constructor(
val refreshAvatar: SharedFlow<Unit> val refreshAvatar: SharedFlow<Unit>
get() = _refreshAvatar.asSharedFlow() get() = _refreshAvatar.asSharedFlow()
init {
viewModelScope.launch(Dispatchers.Default) {
val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false)
_avatarData.update {
AvatarData(
publicKey = hexEncodedPublicKey,
displayName = getDisplayName(),
recipient = recipient
)
}
}
}
fun getDisplayName(): String = fun getDisplayName(): String =
prefs.getProfileName() ?: truncateIdForDisplay(hexEncodedPublicKey) prefs.getProfileName() ?: truncateIdForDisplay(hexEncodedPublicKey)
@ -247,4 +265,10 @@ class SettingsViewModel @Inject constructor(
val hasAvatar: Boolean // true if the user has an avatar set already but is in this temp state because they are trying out a new avatar val hasAvatar: Boolean // true if the user has an avatar set already but is in this temp state because they are trying out a new avatar
) : AvatarDialogState() ) : AvatarDialogState()
} }
data class AvatarData(
val publicKey: String,
val displayName: String,
val recipient: Recipient
)
} }

View File

@ -11,52 +11,46 @@ import kotlinx.coroutines.launch
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import java.io.File import java.io.DataInputStream
import java.io.FileOutputStream import java.io.InputStream
import java.io.FileReader import java.io.InputStreamReader
import java.util.SortedMap import kotlin.math.absoluteValue
import java.util.TreeMap
class IP2Country private constructor(private val context: Context) { private fun ipv4Int(ip: String): UInt =
private val pathsBuiltEventReceiver: BroadcastReceiver ip.split(".", "/", ",").take(4).fold(0U) { acc, s -> acc shl 8 or s.toUInt() }
@OptIn(ExperimentalUnsignedTypes::class)
class IP2Country internal constructor(
private val context: Context,
private val openStream: (String) -> InputStream = context.assets::open
) {
val countryNamesCache = mutableMapOf<String, String>() val countryNamesCache = mutableMapOf<String, String>()
private fun Ipv4Int(ip: String): Int { private val ips: UIntArray by lazy { ipv4ToCountry.first }
var result = 0L private val codes: IntArray by lazy { ipv4ToCountry.second }
var currentValue = 0L
var octetIndex = 0
for (char in ip) { private val ipv4ToCountry: Pair<UIntArray, IntArray> by lazy {
if (char == '.' || char == '/') { openStream("geolite2_country_blocks_ipv4.bin")
result = result or (currentValue shl (8 * (3 - octetIndex))) .let(::DataInputStream)
currentValue = 0 .use {
octetIndex++ val size = it.available() / 8
if (char == '/') break
} else { val ips = UIntArray(size)
currentValue = currentValue * 10 + (char - '0') val codes = IntArray(size)
var i = 0
while (it.available() > 0) {
ips[i] = it.readInt().toUInt()
codes[i] = it.readInt()
i++
}
ips to codes
} }
}
// Handle the last octet
result = result or (currentValue shl (8 * (3 - octetIndex)))
return result.toInt()
}
private val ipv4ToCountry: TreeMap<Int, Int?> by lazy {
val file = loadFile("geolite2_country_blocks_ipv4.csv")
CSVReader(FileReader(file.absoluteFile)).use { csv ->
csv.skip(1)
csv.asSequence().associateTo(TreeMap()) { cols ->
Ipv4Int(cols[0]).toInt() to cols[1].toIntOrNull()
}
}
} }
private val countryToNames: Map<Int, String> by lazy { private val countryToNames: Map<Int, String> by lazy {
val file = loadFile("geolite2_country_locations_english.csv") CSVReader(InputStreamReader(openStream("csv/geolite2_country_locations_english.csv"))).use { csv ->
CSVReader(FileReader(file.absoluteFile)).use { csv ->
csv.skip(1) csv.skip(1)
csv.asSequence() csv.asSequence()
@ -70,68 +64,44 @@ class IP2Country private constructor(private val context: Context) {
// region Initialization // region Initialization
companion object { companion object {
public lateinit var shared: IP2Country lateinit var shared: IP2Country
public val isInitialized: Boolean get() = Companion::shared.isInitialized val isInitialized: Boolean get() = Companion::shared.isInitialized
public fun configureIfNeeded(context: Context) { fun configureIfNeeded(context: Context) {
if (isInitialized) { return; } if (isInitialized) { return; }
shared = IP2Country(context.applicationContext) shared = IP2Country(context.applicationContext)
val pathsBuiltEventReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
shared.populateCacheIfNeeded()
}
}
LocalBroadcastManager.getInstance(context).registerReceiver(pathsBuiltEventReceiver, IntentFilter("pathsBuilt"))
} }
} }
init { init {
populateCacheIfNeeded() populateCacheIfNeeded()
pathsBuiltEventReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
populateCacheIfNeeded()
}
}
LocalBroadcastManager.getInstance(context).registerReceiver(pathsBuiltEventReceiver, IntentFilter("pathsBuilt"))
} }
// TODO: Deinit? // TODO: Deinit?
// endregion // endregion
// region Implementation // region Implementation
private fun loadFile(fileName: String): File { internal fun cacheCountryForIP(ip: String): String? {
val directory = File(context.applicationInfo.dataDir)
val file = File(directory, fileName)
if (directory.list().contains(fileName)) { return file }
val inputStream = context.assets.open("csv/$fileName")
val outputStream = FileOutputStream(file)
val buffer = ByteArray(1024)
while (true) {
val count = inputStream.read(buffer)
if (count < 0) { break }
outputStream.write(buffer, 0, count)
}
inputStream.close()
outputStream.close()
return file
}
private fun cacheCountryForIP(ip: String): String? {
// return early if cached // return early if cached
countryNamesCache[ip]?.let { return it } countryNamesCache[ip]?.let { return it }
val ipInt = Ipv4Int(ip) val ipInt = ipv4Int(ip)
val bestMatchCountry = ipv4ToCountry.floorEntry(ipInt)?.let { (_, code) -> val index = ips.binarySearch(ipInt).let { it.takeIf { it >= 0 } ?: (it.absoluteValue - 2) }
if (code != null) { val code = codes.getOrNull(index)
countryToNames[code] val bestMatchCountry = countryToNames[code]
} else {
null
}
}
if (bestMatchCountry != null) { if (bestMatchCountry != null) countryNamesCache[ip] = bestMatchCountry
countryNamesCache[ip] = bestMatchCountry else Log.d("Loki","Country name for $ip couldn't be found")
return bestMatchCountry
} else { return bestMatchCountry
Log.d("Loki","Country name for $ip couldn't be found")
}
return null
} }
private fun populateCacheIfNeeded() { private fun populateCacheIfNeeded() {
@ -141,10 +111,9 @@ class IP2Country private constructor(private val context: Context) {
cacheCountryForIP(snode.ip) // Preload if needed cacheCountryForIP(snode.ip) // Preload if needed
} }
} }
Log.d("Loki","Cache populated in ${System.currentTimeMillis() - start}ms")
Broadcaster(context).broadcast("onionRequestPathCountriesLoaded") Broadcaster(context).broadcast("onionRequestPathCountriesLoaded")
Log.d("Loki", "Finished preloading onion request path countries.")
} }
} }
// endregion // endregion
} }

View File

@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.util
import android.content.Context
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.Mockito.mock
@RunWith(Parameterized::class)
class IP2CountryTest(
private val ip: String,
private val country: String
) {
private val context: Context = mock(Context::class.java)
private val ip2Country = IP2Country(context, this::class.java.classLoader!!::getResourceAsStream)
@Test
fun getCountryNamesCache() {
assertEquals(country, ip2Country.cacheCountryForIP(ip))
}
companion object {
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Array<Any>> = listOf(
arrayOf("223.121.64.0", "Hong Kong"),
arrayOf("223.121.64.1", "Hong Kong"),
arrayOf("223.121.127.0", "Hong Kong"),
arrayOf("223.121.128.0", "China"),
arrayOf("223.121.129.0", "China"),
arrayOf("223.122.0.0", "Hong Kong"),
arrayOf("223.123.0.0", "Pakistan"),
arrayOf("223.123.128.0", "China"),
arrayOf("223.124.0.0", "China"),
arrayOf("223.128.0.0", "China"),
arrayOf("223.130.0.0", "Singapore")
)
}
}

View File

@ -46,7 +46,8 @@ object OnionRequestAPI {
var guardSnodes = setOf<Snode>() var guardSnodes = setOf<Snode>()
var _paths: AtomicReference<List<Path>?> = AtomicReference(null) var _paths: AtomicReference<List<Path>?> = AtomicReference(null)
var paths: List<Path> // Not a set to ensure we consistently show the same path to the user var paths: List<Path> // Not a Set to ensure we consistently show the same path to the user
@Synchronized
get() { get() {
val paths = _paths.get() val paths = _paths.get()
@ -59,6 +60,7 @@ object OnionRequestAPI {
_paths.set(result) _paths.set(result)
return result return result
} }
@Synchronized
set(newValue) { set(newValue) {
if (newValue.isEmpty()) { if (newValue.isEmpty()) {
database.clearOnionRequestPaths() database.clearOnionRequestPaths()