mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-29 04:55:15 +00:00
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:
commit
7c1efeaaaa
@ -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')
|
||||||
|
|
||||||
|
Can't render this file because it is too large.
|
41
app/ipToCode.gradle.kts
Normal file
41
app/ipToCode.gradle.kts
Normal 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}")
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
@ -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++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the last octet
|
ips to codes
|
||||||
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) countryNamesCache[ip] = bestMatchCountry
|
||||||
}
|
else Log.d("Loki","Country name for $ip couldn't be found")
|
||||||
}
|
|
||||||
|
|
||||||
if (bestMatchCountry != null) {
|
|
||||||
countryNamesCache[ip] = bestMatchCountry
|
|
||||||
return bestMatchCountry
|
return bestMatchCountry
|
||||||
} else {
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user