mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 22:39:25 +00:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
2c7fd320eb | |||
88ee82e1c6 | |||
d6402c624e | |||
8b9c93bc10 | |||
4f5e261d01 | |||
cf455aebe6 | |||
1380d5f8e1 | |||
5f65260bfe | |||
11f0dcfc01 | |||
555b7886a4 | |||
3707a6c0ce | |||
4c8d92b4c3 | |||
8bbd33c896 | |||
ac33a3580f | |||
a64a32fbe6 | |||
9493e97a38 | |||
88bd770260 | |||
a7bd33b7bf | |||
73c724a2e5 | |||
d8cf3c6726 | |||
15dee238b5 | |||
c70626734e |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.17.3
|
||||||
|
|
||||||
|
* `Common`:
|
||||||
|
* Add `fixed` extensions for `Float` and `Double`
|
||||||
|
* New function `emptyDiff`
|
||||||
|
* Now you may pass custom `comparisonFun` to all `diff` functions
|
||||||
|
|
||||||
|
## 0.17.2
|
||||||
|
|
||||||
|
* `FSM`:
|
||||||
|
* `DefaultStatesManager.onUpdateContextsConflictResolver` and `DefaultStatesManager.onStartContextsConflictResolver` now return `false` by default
|
||||||
|
|
||||||
|
## 0.17.1
|
||||||
|
|
||||||
|
* **Hotfix** for absence of jvm dependencies in android modules
|
||||||
|
* `Versions`:
|
||||||
|
* `Ktor`: `2.2.3` -> `2.2.4`
|
||||||
|
|
||||||
|
## 0.17.0
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Kotlin`: `1.7.20` -> `1.8.10`
|
||||||
|
* `Serialization`: `1.4.1` -> `1.5.0`
|
||||||
|
* `KSLog`: `0.5.4` -> `1.0.0`
|
||||||
|
* `AppCompat`: `1.6.0` -> `1.6.1`
|
||||||
|
|
||||||
## 0.16.13
|
## 0.16.13
|
||||||
|
|
||||||
* `Repos`:
|
* `Repos`:
|
||||||
|
@@ -18,6 +18,7 @@ kotlin {
|
|||||||
api project(":micro_utils.coroutines")
|
api project(":micro_utils.coroutines")
|
||||||
api libs.android.fragment
|
api libs.android.fragment
|
||||||
}
|
}
|
||||||
|
dependsOn jvmMain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,11 @@ data class Diff<T> internal constructor(
|
|||||||
*/
|
*/
|
||||||
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
|
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
|
||||||
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
||||||
)
|
) {
|
||||||
|
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList())
|
||||||
|
|
||||||
private inline fun <T> performChanges(
|
private inline fun <T> performChanges(
|
||||||
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
||||||
@@ -43,14 +47,14 @@ private inline fun <T> performChanges(
|
|||||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||||
removedList: MutableList<IndexedValue<T>>,
|
removedList: MutableList<IndexedValue<T>>,
|
||||||
addedList: MutableList<IndexedValue<T>>,
|
addedList: MutableList<IndexedValue<T>>,
|
||||||
strictComparison: Boolean
|
comparisonFun: (T?, T?) -> Boolean
|
||||||
) {
|
) {
|
||||||
var i = -1
|
var i = -1
|
||||||
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
||||||
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
||||||
i++
|
i++
|
||||||
val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison)
|
val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value)
|
||||||
val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison)
|
val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value)
|
||||||
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
||||||
changedList.addAll(
|
changedList.addAll(
|
||||||
potentialChanges.take(i).mapNotNull {
|
potentialChanges.take(i).mapNotNull {
|
||||||
@@ -104,7 +108,7 @@ private inline fun <T> performChanges(
|
|||||||
*/
|
*/
|
||||||
fun <T> Iterable<T>.calculateDiff(
|
fun <T> Iterable<T>.calculateDiff(
|
||||||
other: Iterable<T>,
|
other: Iterable<T>,
|
||||||
strictComparison: Boolean = false
|
comparisonFun: (T?, T?) -> Boolean
|
||||||
): Diff<T> {
|
): Diff<T> {
|
||||||
var i = -1
|
var i = -1
|
||||||
var j = -1
|
var j = -1
|
||||||
@@ -132,7 +136,7 @@ fun <T> Iterable<T>.calculateDiff(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
comparisonFun(oldObject, newObject) -> {
|
||||||
changedObjects.addAll(potentiallyChangedObjects.map {
|
changedObjects.addAll(potentiallyChangedObjects.map {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
||||||
@@ -143,23 +147,49 @@ fun <T> Iterable<T>.calculateDiff(
|
|||||||
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
|
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
|
||||||
val previousOldsAdditionsSize = additionalInOld.size
|
val previousOldsAdditionsSize = additionalInOld.size
|
||||||
val previousNewsAdditionsSize = additionalInNew.size
|
val previousNewsAdditionsSize = additionalInNew.size
|
||||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||||
i -= (additionalInOld.size - previousOldsAdditionsSize)
|
i -= (additionalInOld.size - previousOldsAdditionsSize)
|
||||||
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
potentiallyChangedObjects.add(null to null)
|
potentiallyChangedObjects.add(null to null)
|
||||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||||
|
|
||||||
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculating [Diff] object
|
||||||
|
*
|
||||||
|
* @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different
|
||||||
|
* objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links
|
||||||
|
*/
|
||||||
|
fun <T> Iterable<T>.calculateDiff(
|
||||||
|
other: Iterable<T>,
|
||||||
|
strictComparison: Boolean = false
|
||||||
|
): Diff<T> = calculateDiff(
|
||||||
|
other,
|
||||||
|
comparisonFun = if (strictComparison) {
|
||||||
|
{ t1, t2 ->
|
||||||
|
t1 === t2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{ t1, t2 ->
|
||||||
|
t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
inline fun <T> Iterable<T>.diff(
|
inline fun <T> Iterable<T>.diff(
|
||||||
other: Iterable<T>,
|
other: Iterable<T>,
|
||||||
strictComparison: Boolean = false
|
strictComparison: Boolean = false
|
||||||
): Diff<T> = calculateDiff(other, strictComparison)
|
): Diff<T> = calculateDiff(other, strictComparison)
|
||||||
|
inline fun <T> Iterable<T>.diff(
|
||||||
|
other: Iterable<T>,
|
||||||
|
noinline comparisonFun: (T?, T?) -> Boolean
|
||||||
|
): Diff<T> = calculateDiff(other, comparisonFun)
|
||||||
|
|
||||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
|
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false)
|
||||||
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,3 +217,22 @@ fun <T> MutableList<T>.applyDiff(
|
|||||||
set(new.index, new.value)
|
set(new.index, new.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
||||||
|
* mutable list
|
||||||
|
*/
|
||||||
|
fun <T> MutableList<T>.applyDiff(
|
||||||
|
source: Iterable<T>,
|
||||||
|
comparisonFun: (T?, T?) -> Boolean
|
||||||
|
): Diff<T> = calculateDiff(source, comparisonFun).also {
|
||||||
|
for (i in it.removed.indices.sortedDescending()) {
|
||||||
|
removeAt(it.removed[i].index)
|
||||||
|
}
|
||||||
|
it.added.forEach { (i, t) ->
|
||||||
|
add(i, t)
|
||||||
|
}
|
||||||
|
it.replaced.forEach { (_, new) ->
|
||||||
|
set(new.index, new.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
val FixedSignsRange = 0 .. 100
|
||||||
|
|
||||||
|
expect fun Float.fixed(signs: Int): Float
|
||||||
|
expect fun Double.fixed(signs: Int): Double
|
@@ -0,0 +1,4 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
actual fun Float.fixed(signs: Int): Float = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toFloat()
|
||||||
|
actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toDouble()
|
@@ -0,0 +1,12 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.math.RoundingMode
|
||||||
|
|
||||||
|
actual fun Float.fixed(signs: Int): Float = BigDecimal.valueOf(this.toDouble())
|
||||||
|
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
||||||
|
.toFloat();
|
||||||
|
|
||||||
|
actual fun Double.fixed(signs: Int): Double = BigDecimal.valueOf(this)
|
||||||
|
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
||||||
|
.toDouble();
|
@@ -1,30 +1,5 @@
|
|||||||
apply plugin: 'com.getkeepsafe.dexcount'
|
apply plugin: 'com.getkeepsafe.dexcount'
|
||||||
|
|
||||||
ext {
|
|
||||||
jvmKotlinFolderFile = {
|
|
||||||
String sep = File.separator
|
|
||||||
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
|
|
||||||
}
|
|
||||||
|
|
||||||
enableIncludingJvmCodeInAndroidPart = {
|
|
||||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
|
||||||
if (jvmKotlinFolder.exists()) {
|
|
||||||
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disableIncludingJvmCodeInAndroidPart = {
|
|
||||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
|
||||||
String[] oldDirs = android.sourceSets.main.java.srcDirs
|
|
||||||
android.sourceSets.main.java.srcDirs = []
|
|
||||||
for (oldDir in oldDirs) {
|
|
||||||
if (oldDir != jvmKotlinFolder.path) {
|
|
||||||
android.sourceSets.main.java.srcDirs += oldDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||||
buildToolsVersion libs.versions.android.props.buildTools.get()
|
buildToolsVersion libs.versions.android.props.buildTools.get()
|
||||||
@@ -58,10 +33,4 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
String sep = File.separator
|
|
||||||
main.java.srcDirs += "src${sep}main${sep}kotlin"
|
|
||||||
enableIncludingJvmCodeInAndroidPart()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -48,8 +48,8 @@ interface DefaultStatesManagerRepo<T : State> {
|
|||||||
*/
|
*/
|
||||||
open class DefaultStatesManager<T : State>(
|
open class DefaultStatesManager<T : State>(
|
||||||
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
|
||||||
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
|
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> false },
|
||||||
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
|
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> false }
|
||||||
) : StatesManager<T> {
|
) : StatesManager<T> {
|
||||||
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
|
||||||
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
|
||||||
|
@@ -14,5 +14,5 @@ crypto_js_version=4.1.1
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.16.13
|
version=0.17.3
|
||||||
android_code_version=181
|
android_code_version=185
|
||||||
|
@@ -1,36 +1,36 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
kt = "1.7.20"
|
kt = "1.8.10"
|
||||||
kt-serialization = "1.4.1"
|
kt-serialization = "1.5.0"
|
||||||
kt-coroutines = "1.6.4"
|
kt-coroutines = "1.6.4"
|
||||||
|
|
||||||
kslog = "0.5.4"
|
kslog = "1.0.0"
|
||||||
|
|
||||||
jb-compose = "1.2.2"
|
jb-compose = "1.3.1-rc01"
|
||||||
jb-exposed = "0.41.1"
|
jb-exposed = "0.41.1"
|
||||||
jb-dokka = "1.7.20"
|
jb-dokka = "1.7.20"
|
||||||
|
|
||||||
klock = "3.4.0"
|
klock = "3.4.0"
|
||||||
uuid = "0.6.0"
|
uuid = "0.7.0"
|
||||||
|
|
||||||
ktor = "2.2.3"
|
ktor = "2.2.4"
|
||||||
|
|
||||||
gh-release = "2.4.1"
|
gh-release = "2.4.1"
|
||||||
|
|
||||||
koin = "3.3.2"
|
koin = "3.3.2"
|
||||||
|
|
||||||
ksp = "1.7.20-1.0.8"
|
ksp = "1.8.10-1.0.9"
|
||||||
kotlin-poet = "1.12.0"
|
kotlin-poet = "1.12.0"
|
||||||
|
|
||||||
android-gradle = "7.3.0"
|
android-gradle = "7.3.1"
|
||||||
dexcount = "3.1.0"
|
dexcount = "4.0.0"
|
||||||
|
|
||||||
android-coreKtx = "1.9.0"
|
android-coreKtx = "1.9.0"
|
||||||
android-recyclerView = "1.2.1"
|
android-recyclerView = "1.2.1"
|
||||||
android-appCompat = "1.6.0"
|
android-appCompat = "1.6.1"
|
||||||
android-fragment = "1.5.5"
|
android-fragment = "1.5.5"
|
||||||
android-espresso = "3.4.0"
|
android-espresso = "3.5.1"
|
||||||
android-test = "1.1.3"
|
android-test = "1.1.5"
|
||||||
|
|
||||||
android-props-minSdk = "21"
|
android-props-minSdk = "21"
|
||||||
android-props-compileSdk = "33"
|
android-props-compileSdk = "33"
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@@ -15,7 +15,6 @@ kotlin {
|
|||||||
api libs.ktor.client
|
api libs.ktor.client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidMain {
|
androidMain {
|
||||||
dependsOn jvmMain
|
dependsOn jvmMain
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,8 @@ actual suspend fun <T> HttpClient.uniUpload(
|
|||||||
val withBinary = data.values.any { it is File || it is UniUploadFileInfo }
|
val withBinary = data.values.any { it is File || it is UniUploadFileInfo }
|
||||||
|
|
||||||
val formData = formData {
|
val formData = formData {
|
||||||
data.forEach { (k, v) ->
|
for (k in data.keys) {
|
||||||
|
val v = data[k] ?: continue
|
||||||
when (v) {
|
when (v) {
|
||||||
is File -> append(
|
is File -> append(
|
||||||
k,
|
k,
|
||||||
@@ -89,7 +90,7 @@ actual suspend fun <T> HttpClient.uniUpload(
|
|||||||
submitForm(
|
submitForm(
|
||||||
url,
|
url,
|
||||||
Parameters.build {
|
Parameters.build {
|
||||||
formData.forEach {
|
for (it in formData) {
|
||||||
val formItem = (it as PartData.FormItem)
|
val formItem = (it as PartData.FormItem)
|
||||||
append(it.name!!, it.value)
|
append(it.name!!, it.value)
|
||||||
}
|
}
|
||||||
|
@@ -17,5 +17,8 @@ kotlin {
|
|||||||
api libs.ktor.io
|
api libs.ktor.io
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
androidMain {
|
||||||
|
dependsOn jvmMain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -50,6 +50,8 @@ kotlin {
|
|||||||
implementation libs.android.espresso
|
implementation libs.android.espresso
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidMain.dependsOn jvmMain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -61,6 +61,8 @@ kotlin {
|
|||||||
implementation libs.android.espresso
|
implementation libs.android.espresso
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidMain.dependsOn jvmMain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -32,5 +32,3 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disableIncludingJvmCodeInAndroidPart()
|
|
||||||
|
Reference in New Issue
Block a user