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'
test.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 {
@ -246,6 +253,12 @@ android {
}
}
apply {
from("ipToCode.gradle.kts")
}
preBuild.dependsOn ipToCode
dependencies {
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 additionalPublicKey: String? = null
var additionalDisplayName: String? = null
var recipient: Recipient? = null
private val profilePicturesCache = mutableMapOf<View, Recipient>()
private val resourcePadding by lazy {
@ -51,6 +52,7 @@ class ProfilePictureView @JvmOverloads constructor(
}
fun update(recipient: Recipient) {
this.recipient = recipient
recipient.run { update(address, isLegacyClosedGroupRecipient, isOpenGroupInboxRecipient) }
}
@ -121,7 +123,14 @@ class ProfilePictureView @JvmOverloads constructor(
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) {
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
profilePicturesCache[imageView] = recipient
val signalProfilePicture = recipient.contactPhoto

View File

@ -173,11 +173,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
binding.run {
profilePictureView.apply {
publicKey = viewModel.hexEncodedPublicKey
displayName = viewModel.getDisplayName()
update()
}
profilePictureView.setOnClickListener {
binding.avatarDialog.isVisible = true
showAvatarDialog = true
@ -209,6 +204,18 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
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() {

View File

@ -25,6 +25,7 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
import org.session.libsignal.utilities.Log
@ -50,7 +51,7 @@ class SettingsViewModel @Inject constructor(
private var tempFile: File? = null
val hexEncodedPublicKey: String get() = prefs.getLocalNumber() ?: ""
val hexEncodedPublicKey: String = prefs.getLocalNumber() ?: ""
private val userAddress = Address.fromSerialized(hexEncodedPublicKey)
@ -68,6 +69,10 @@ class SettingsViewModel @Inject constructor(
val recoveryHidden: StateFlow<Boolean>
get() = _recoveryHidden
private val _avatarData: MutableStateFlow<AvatarData?> = MutableStateFlow(null)
val avatarData: StateFlow<AvatarData?>
get() = _avatarData
/**
* Refreshes the avatar on the main settings page
*/
@ -75,6 +80,19 @@ class SettingsViewModel @Inject constructor(
val refreshAvatar: SharedFlow<Unit>
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 =
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
) : 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.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils
import java.io.File
import java.io.FileOutputStream
import java.io.FileReader
import java.util.SortedMap
import java.util.TreeMap
import java.io.DataInputStream
import java.io.InputStream
import java.io.InputStreamReader
import kotlin.math.absoluteValue
class IP2Country private constructor(private val context: Context) {
private val pathsBuiltEventReceiver: BroadcastReceiver
private fun ipv4Int(ip: String): UInt =
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>()
private fun Ipv4Int(ip: String): Int {
var result = 0L
var currentValue = 0L
var octetIndex = 0
private val ips: UIntArray by lazy { ipv4ToCountry.first }
private val codes: IntArray by lazy { ipv4ToCountry.second }
for (char in ip) {
if (char == '.' || char == '/') {
result = result or (currentValue shl (8 * (3 - octetIndex)))
currentValue = 0
octetIndex++
if (char == '/') break
} else {
currentValue = currentValue * 10 + (char - '0')
private val ipv4ToCountry: Pair<UIntArray, IntArray> by lazy {
openStream("geolite2_country_blocks_ipv4.bin")
.let(::DataInputStream)
.use {
val size = it.available() / 8
val ips = UIntArray(size)
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 {
val file = loadFile("geolite2_country_locations_english.csv")
CSVReader(FileReader(file.absoluteFile)).use { csv ->
CSVReader(InputStreamReader(openStream("csv/geolite2_country_locations_english.csv"))).use { csv ->
csv.skip(1)
csv.asSequence()
@ -70,68 +64,44 @@ class IP2Country private constructor(private val context: Context) {
// region Initialization
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; }
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 {
populateCacheIfNeeded()
pathsBuiltEventReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
populateCacheIfNeeded()
}
}
LocalBroadcastManager.getInstance(context).registerReceiver(pathsBuiltEventReceiver, IntentFilter("pathsBuilt"))
}
// TODO: Deinit?
// endregion
// region Implementation
private fun loadFile(fileName: String): File {
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? {
internal fun cacheCountryForIP(ip: String): String? {
// return early if cached
countryNamesCache[ip]?.let { return it }
val ipInt = Ipv4Int(ip)
val bestMatchCountry = ipv4ToCountry.floorEntry(ipInt)?.let { (_, code) ->
if (code != null) {
countryToNames[code]
} else {
null
}
}
val ipInt = ipv4Int(ip)
val index = ips.binarySearch(ipInt).let { it.takeIf { it >= 0 } ?: (it.absoluteValue - 2) }
val code = codes.getOrNull(index)
val bestMatchCountry = countryToNames[code]
if (bestMatchCountry != null) {
countryNamesCache[ip] = bestMatchCountry
return bestMatchCountry
} else {
Log.d("Loki","Country name for $ip couldn't be found")
}
return null
if (bestMatchCountry != null) countryNamesCache[ip] = bestMatchCountry
else Log.d("Loki","Country name for $ip couldn't be found")
return bestMatchCountry
}
private fun populateCacheIfNeeded() {
@ -141,10 +111,9 @@ class IP2Country private constructor(private val context: Context) {
cacheCountryForIP(snode.ip) // Preload if needed
}
}
Log.d("Loki","Cache populated in ${System.currentTimeMillis() - start}ms")
Broadcaster(context).broadcast("onionRequestPathCountriesLoaded")
Log.d("Loki", "Finished preloading onion request path countries.")
}
}
// 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 _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() {
val paths = _paths.get()
@ -59,6 +60,7 @@ object OnionRequestAPI {
_paths.set(result)
return result
}
@Synchronized
set(newValue) {
if (newValue.isEmpty()) {
database.clearOnionRequestPaths()