forked from github/szkolny
1261 lines
44 KiB
Kotlin
1261 lines
44 KiB
Kotlin
package pl.szczodrzynski.edziennik
|
|
|
|
import android.Manifest
|
|
import android.app.Activity
|
|
import android.content.ClipData
|
|
import android.content.ClipboardManager
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.content.res.ColorStateList
|
|
import android.content.res.Resources
|
|
import android.database.Cursor
|
|
import android.graphics.PorterDuff
|
|
import android.graphics.PorterDuffColorFilter
|
|
import android.graphics.Rect
|
|
import android.graphics.Typeface
|
|
import android.graphics.drawable.Drawable
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.text.*
|
|
import android.text.style.ForegroundColorSpan
|
|
import android.text.style.StrikethroughSpan
|
|
import android.text.style.StyleSpan
|
|
import android.util.*
|
|
import android.util.Base64
|
|
import android.util.Base64.NO_WRAP
|
|
import android.util.Base64.encodeToString
|
|
import android.view.View
|
|
import android.view.WindowManager
|
|
import android.widget.*
|
|
import androidx.annotation.*
|
|
import androidx.core.app.ActivityCompat
|
|
import androidx.core.database.getIntOrNull
|
|
import androidx.core.database.getLongOrNull
|
|
import androidx.core.database.getStringOrNull
|
|
import androidx.core.util.forEach
|
|
import androidx.lifecycle.LifecycleOwner
|
|
import androidx.lifecycle.LiveData
|
|
import androidx.lifecycle.Observer
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
import androidx.viewpager.widget.ViewPager
|
|
import com.google.android.gms.security.ProviderInstaller
|
|
import com.google.android.material.button.MaterialButton
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
import com.google.gson.JsonArray
|
|
import com.google.gson.JsonElement
|
|
import com.google.gson.JsonObject
|
|
import com.google.gson.JsonParser
|
|
import im.wangchao.mhttp.Response
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
import okhttp3.ConnectionSpec
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.RequestBody
|
|
import okhttp3.TlsVersion
|
|
import okio.Buffer
|
|
import pl.szczodrzynski.edziennik.data.api.*
|
|
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
|
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException
|
|
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
|
|
import pl.szczodrzynski.edziennik.data.db.entity.Notification
|
|
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
|
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
|
|
import pl.szczodrzynski.edziennik.data.db.entity.Team
|
|
import pl.szczodrzynski.edziennik.network.TLSSocketFactory
|
|
import pl.szczodrzynski.edziennik.utils.models.Time
|
|
import java.io.InterruptedIOException
|
|
import java.io.PrintWriter
|
|
import java.io.StringWriter
|
|
import java.math.BigInteger
|
|
import java.net.ConnectException
|
|
import java.net.SocketTimeoutException
|
|
import java.net.UnknownHostException
|
|
import java.nio.charset.Charset
|
|
import java.security.KeyStore
|
|
import java.security.MessageDigest
|
|
import java.text.SimpleDateFormat
|
|
import java.util.*
|
|
import java.util.zip.CRC32
|
|
import javax.crypto.Mac
|
|
import javax.crypto.spec.SecretKeySpec
|
|
import javax.net.ssl.SSLContext
|
|
import javax.net.ssl.SSLException
|
|
import javax.net.ssl.TrustManagerFactory
|
|
import javax.net.ssl.X509TrustManager
|
|
import kotlin.Pair
|
|
|
|
|
|
fun List<Teacher>.byId(id: Long) = firstOrNull { it.id == id }
|
|
fun List<Teacher>.byNameFirstLast(nameFirstLast: String) = firstOrNull { it.name + " " + it.surname == nameFirstLast }
|
|
fun List<Teacher>.byNameLastFirst(nameLastFirst: String) = firstOrNull { it.surname + " " + it.name == nameLastFirst }
|
|
fun List<Teacher>.byNameFDotLast(nameFDotLast: String) = firstOrNull { it.name + "." + it.surname == nameFDotLast }
|
|
fun List<Teacher>.byNameFDotSpaceLast(nameFDotSpaceLast: String) = firstOrNull { it.name + ". " + it.surname == nameFDotSpaceLast }
|
|
|
|
fun JsonObject?.get(key: String): JsonElement? = this?.get(key)
|
|
|
|
fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (it.isJsonNull) null else it.asBoolean }
|
|
fun JsonObject?.getString(key: String): String? = get(key)?.let { if (it.isJsonNull) null else it.asString }
|
|
fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (it.isJsonNull) null else it.asInt }
|
|
fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (it.isJsonNull) null else it.asLong }
|
|
fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(it.isJsonNull) null else it.asFloat }
|
|
fun JsonObject?.getChar(key: String): Char? = get(key)?.let { if(it.isJsonNull) null else it.asCharacter }
|
|
fun JsonObject?.getJsonObject(key: String): JsonObject? = get(key)?.let { if (it.isJsonObject) it.asJsonObject else null }
|
|
fun JsonObject?.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonArray) it.asJsonArray else null }
|
|
|
|
fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (it.isJsonNull) defaultValue else it.asBoolean } ?: defaultValue
|
|
fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (it.isJsonNull) defaultValue else it.asString } ?: defaultValue
|
|
fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (it.isJsonNull) defaultValue else it.asInt } ?: defaultValue
|
|
fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (it.isJsonNull) defaultValue else it.asLong } ?: defaultValue
|
|
fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(it.isJsonNull) defaultValue else it.asFloat } ?: defaultValue
|
|
fun JsonObject?.getChar(key: String, defaultValue: Char): Char = get(key)?.let { if(it.isJsonNull) defaultValue else it.asCharacter } ?: defaultValue
|
|
fun JsonObject?.getJsonObject(key: String, defaultValue: JsonObject): JsonObject = get(key)?.let { if (it.isJsonObject) it.asJsonObject else defaultValue } ?: defaultValue
|
|
fun JsonObject?.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonArray) it.asJsonArray else defaultValue } ?: defaultValue
|
|
|
|
fun JsonArray.getBoolean(key: Int): Boolean? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asBoolean }
|
|
fun JsonArray.getString(key: Int): String? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asString }
|
|
fun JsonArray.getInt(key: Int): Int? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asInt }
|
|
fun JsonArray.getLong(key: Int): Long? = if (key >= size()) null else get(key)?.let { if (it.isJsonNull) null else it.asLong }
|
|
fun JsonArray.getFloat(key: Int): Float? = if (key >= size()) null else get(key)?.let { if(it.isJsonNull) null else it.asFloat }
|
|
fun JsonArray.getChar(key: Int): Char? = if (key >= size()) null else get(key)?.let { if(it.isJsonNull) null else it.asCharacter }
|
|
fun JsonArray.getJsonObject(key: Int): JsonObject? = if (key >= size()) null else get(key)?.let { if (it.isJsonObject) it.asJsonObject else null }
|
|
fun JsonArray.getJsonArray(key: Int): JsonArray? = if (key >= size()) null else get(key)?.let { if (it.isJsonArray) it.asJsonArray else null }
|
|
|
|
fun String.toJsonObject(): JsonObject? = try { JsonParser().parse(this).asJsonObject } catch (ignore: Exception) { null }
|
|
|
|
operator fun JsonObject.set(key: String, value: JsonElement) = this.add(key, value)
|
|
operator fun JsonObject.set(key: String, value: Boolean) = this.addProperty(key, value)
|
|
operator fun JsonObject.set(key: String, value: String?) = this.addProperty(key, value)
|
|
operator fun JsonObject.set(key: String, value: Number) = this.addProperty(key, value)
|
|
operator fun JsonObject.set(key: String, value: Char) = this.addProperty(key, value)
|
|
|
|
operator fun Profile.set(key: String, value: JsonElement) = this.studentData.add(key, value)
|
|
operator fun Profile.set(key: String, value: Boolean) = this.studentData.addProperty(key, value)
|
|
operator fun Profile.set(key: String, value: String?) = this.studentData.addProperty(key, value)
|
|
operator fun Profile.set(key: String, value: Number) = this.studentData.addProperty(key, value)
|
|
operator fun Profile.set(key: String, value: Char) = this.studentData.addProperty(key, value)
|
|
|
|
fun JsonArray.asJsonObjectList() = this.mapNotNull { it.asJsonObject }
|
|
|
|
fun CharSequence?.isNotNullNorEmpty(): Boolean {
|
|
return this != null && this.isNotEmpty()
|
|
}
|
|
|
|
fun <T> Collection<T>?.isNotNullNorEmpty(): Boolean {
|
|
return this != null && this.isNotEmpty()
|
|
}
|
|
|
|
fun CharSequence?.isNotNullNorBlank(): Boolean {
|
|
return this != null && this.isNotBlank()
|
|
}
|
|
|
|
fun currentTimeUnix() = System.currentTimeMillis() / 1000
|
|
|
|
fun Bundle?.getInt(key: String, defaultValue: Int): Int {
|
|
return this?.getInt(key, defaultValue) ?: defaultValue
|
|
}
|
|
fun Bundle?.getLong(key: String, defaultValue: Long): Long {
|
|
return this?.getLong(key, defaultValue) ?: defaultValue
|
|
}
|
|
fun Bundle?.getFloat(key: String, defaultValue: Float): Float {
|
|
return this?.getFloat(key, defaultValue) ?: defaultValue
|
|
}
|
|
fun Bundle?.getString(key: String, defaultValue: String): String {
|
|
return this?.getString(key, defaultValue) ?: defaultValue
|
|
}
|
|
|
|
fun Bundle?.getIntOrNull(key: String): Int? {
|
|
return this?.get(key) as? Int
|
|
}
|
|
fun <T : Any> Bundle?.get(key: String): T? {
|
|
return this?.get(key) as? T?
|
|
}
|
|
|
|
/**
|
|
* ` The quick BROWN_fox Jumps OveR THE LAZy-DOG. `
|
|
*
|
|
* converts to
|
|
*
|
|
* `The Quick Brown_fox Jumps Over The Lazy-Dog.`
|
|
*/
|
|
fun String?.fixName(): String {
|
|
return this?.fixWhiteSpaces()?.toProperCase() ?: ""
|
|
}
|
|
|
|
/**
|
|
* `The quick BROWN_fox Jumps OveR THE LAZy-DOG.`
|
|
*
|
|
* converts to
|
|
*
|
|
* `The Quick Brown_fox Jumps Over The Lazy-Dog.`
|
|
*/
|
|
fun String.toProperCase(): String = changeStringCase(this)
|
|
|
|
/**
|
|
* `John Smith` -> `Smith John`
|
|
*
|
|
* `JOHN SMith` -> `SMith JOHN`
|
|
*/
|
|
fun String.swapFirstLastName(): String {
|
|
return this.split(" ").let {
|
|
if (it.size > 1)
|
|
it[1]+" "+it[0]
|
|
else
|
|
it[0]
|
|
}
|
|
}
|
|
|
|
fun String.splitName(): Pair<String, String>? {
|
|
return this.split(" ").let {
|
|
if (it.size >= 2) Pair(it[0], it[1])
|
|
else null
|
|
}
|
|
}
|
|
|
|
fun changeStringCase(s: String): String {
|
|
val delimiters = " '-/"
|
|
val sb = StringBuilder()
|
|
var capNext = true
|
|
for (ch in s.toCharArray()) {
|
|
var c = ch
|
|
c = if (capNext)
|
|
Character.toUpperCase(c)
|
|
else
|
|
Character.toLowerCase(c)
|
|
sb.append(c)
|
|
capNext = delimiters.indexOf(c) >= 0
|
|
}
|
|
return sb.toString()
|
|
}
|
|
|
|
fun buildFullName(firstName: String?, lastName: String?): String {
|
|
return "$firstName $lastName".fixName()
|
|
}
|
|
|
|
fun String.getShortName(): String {
|
|
return split(" ").let {
|
|
if (it.size > 1)
|
|
"${it[0]} ${it[1][0]}."
|
|
else
|
|
it[0]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* "John Smith" -> "JS"
|
|
*
|
|
* "JOHN SMith" -> "JS"
|
|
*
|
|
* "John" -> "J"
|
|
*
|
|
* "John " -> "J"
|
|
*
|
|
* "John Smith " -> "JS"
|
|
*
|
|
* " " -> ""
|
|
*
|
|
* " " -> ""
|
|
*/
|
|
fun String?.getNameInitials(): String {
|
|
if (this.isNullOrBlank()) return ""
|
|
return this.toUpperCase().fixWhiteSpaces().split(" ").take(2).map { it[0] }.joinToString("")
|
|
}
|
|
|
|
fun List<String>.join(delimiter: String): String {
|
|
return concat(delimiter).toString()
|
|
}
|
|
|
|
fun colorFromName(name: String?): Int {
|
|
val i = (name ?: "").crc32()
|
|
return when ((i / 10 % 16 + 1).toInt()) {
|
|
13 -> 0xffF44336
|
|
4 -> 0xffF50057
|
|
2 -> 0xffD500F9
|
|
9 -> 0xff6200EA
|
|
5 -> 0xffFFAB00
|
|
1 -> 0xff304FFE
|
|
6 -> 0xff40C4FF
|
|
14 -> 0xff26A69A
|
|
15 -> 0xff00C853
|
|
7 -> 0xffFFD600
|
|
3 -> 0xffFF3D00
|
|
8 -> 0xffDD2C00
|
|
10 -> 0xff795548
|
|
12 -> 0xff2979FF
|
|
11 -> 0xffFF6D00
|
|
else -> 0xff64DD17
|
|
}.toInt()
|
|
}
|
|
|
|
fun colorFromCssName(name: String): Int {
|
|
return when (name) {
|
|
"red" -> 0xffff0000
|
|
"green" -> 0xff008000
|
|
"blue" -> 0xff0000ff
|
|
"violet" -> 0xffee82ee
|
|
"brown" -> 0xffa52a2a
|
|
"orange" -> 0xffffa500
|
|
"black" -> 0xff000000
|
|
"white" -> 0xffffffff
|
|
else -> -1L
|
|
}.toInt()
|
|
}
|
|
|
|
fun List<Profile>.filterOutArchived() = this.filter { !it.archived }
|
|
|
|
fun Activity.isStoragePermissionGranted(): Boolean {
|
|
return if (Build.VERSION.SDK_INT >= 23) {
|
|
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
|
true
|
|
} else {
|
|
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
|
|
false
|
|
}
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
fun Response?.getUnixDate(): Long {
|
|
val rfcDate = this?.headers()?.get("date") ?: return currentTimeUnix()
|
|
val pattern = "EEE, dd MMM yyyy HH:mm:ss Z"
|
|
val format = SimpleDateFormat(pattern, Locale.ENGLISH)
|
|
return format.parse(rfcDate).time / 1000
|
|
}
|
|
|
|
const val MINUTE = 60L
|
|
const val HOUR = 60L*MINUTE
|
|
const val DAY = 24L*HOUR
|
|
const val WEEK = 7L*DAY
|
|
const val MONTH = 30L*DAY
|
|
const val YEAR = 365L*DAY
|
|
const val MS = 1000L
|
|
|
|
fun <T> LongSparseArray<T>.values(): List<T> {
|
|
val result = mutableListOf<T>()
|
|
forEach { _, value ->
|
|
result += value
|
|
}
|
|
return result
|
|
}
|
|
|
|
fun SparseArray<*>.keys(): List<Int> {
|
|
val result = mutableListOf<Int>()
|
|
forEach { key, _ ->
|
|
result += key
|
|
}
|
|
return result
|
|
}
|
|
fun <T> SparseArray<T>.values(): List<T> {
|
|
val result = mutableListOf<T>()
|
|
forEach { _, value ->
|
|
result += value
|
|
}
|
|
return result
|
|
}
|
|
|
|
fun SparseIntArray.keys(): List<Int> {
|
|
val result = mutableListOf<Int>()
|
|
forEach { key, _ ->
|
|
result += key
|
|
}
|
|
return result
|
|
}
|
|
fun SparseIntArray.values(): List<Int> {
|
|
val result = mutableListOf<Int>()
|
|
forEach { _, value ->
|
|
result += value
|
|
}
|
|
return result
|
|
}
|
|
|
|
fun <K, V> List<Pair<K, V>>.keys(): List<K> {
|
|
val result = mutableListOf<K>()
|
|
forEach { pair ->
|
|
result += pair.first
|
|
}
|
|
return result
|
|
}
|
|
fun <K, V> List<Pair<K, V>>.values(): List<V> {
|
|
val result = mutableListOf<V>()
|
|
forEach { pair ->
|
|
result += pair.second
|
|
}
|
|
return result
|
|
}
|
|
|
|
fun <T> List<T>.toSparseArray(destination: SparseArray<T>, key: (T) -> Int) {
|
|
forEach {
|
|
destination.put(key(it), it)
|
|
}
|
|
}
|
|
fun <T> List<T>.toSparseArray(destination: LongSparseArray<T>, key: (T) -> Long) {
|
|
forEach {
|
|
destination.put(key(it), it)
|
|
}
|
|
}
|
|
|
|
fun <T> List<T>.toSparseArray(key: (T) -> Int): SparseArray<T> {
|
|
val result = SparseArray<T>()
|
|
toSparseArray(result, key)
|
|
return result
|
|
}
|
|
fun <T> List<T>.toSparseArray(key: (T) -> Long): LongSparseArray<T> {
|
|
val result = LongSparseArray<T>()
|
|
toSparseArray(result, key)
|
|
return result
|
|
}
|
|
|
|
fun <T> SparseArray<T>.singleOrNull(predicate: (T) -> Boolean): T? {
|
|
forEach { _, value ->
|
|
if (predicate(value))
|
|
return value
|
|
}
|
|
return null
|
|
}
|
|
fun <T> LongSparseArray<T>.singleOrNull(predicate: (T) -> Boolean): T? {
|
|
forEach { _, value ->
|
|
if (predicate(value))
|
|
return value
|
|
}
|
|
return null
|
|
}
|
|
|
|
fun String.fixWhiteSpaces() = buildString(length) {
|
|
var wasWhiteSpace = true
|
|
for (c in this@fixWhiteSpaces) {
|
|
if (c.isWhitespace()) {
|
|
if (!wasWhiteSpace) {
|
|
append(c)
|
|
wasWhiteSpace = true
|
|
}
|
|
} else {
|
|
append(c)
|
|
wasWhiteSpace = false
|
|
}
|
|
}
|
|
}.trimEnd()
|
|
|
|
fun List<Team>.getById(id: Long): Team? {
|
|
return singleOrNull { it.id == id }
|
|
}
|
|
fun LongSparseArray<Team>.getById(id: Long): Team? {
|
|
forEach { _, value ->
|
|
if (value.id == id)
|
|
return value
|
|
}
|
|
return null
|
|
}
|
|
|
|
operator fun MatchResult.get(group: Int): String {
|
|
if (group >= groupValues.size)
|
|
return ""
|
|
return groupValues[group]
|
|
}
|
|
|
|
fun Context.setLanguage(language: String) {
|
|
val locale = Locale(language.toLowerCase(Locale.ROOT))
|
|
val configuration = resources.configuration
|
|
Locale.setDefault(locale)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
|
configuration.setLocale(locale)
|
|
}
|
|
configuration.locale = locale
|
|
resources.updateConfiguration(configuration, resources.displayMetrics)
|
|
}
|
|
|
|
/*
|
|
Code copied from android-28/java.util.Locale.initDefault()
|
|
*/
|
|
fun initDefaultLocale() {
|
|
run {
|
|
// user.locale gets priority
|
|
/*val languageTag: String? = System.getProperty("user.locale", "")
|
|
if (languageTag.isNotNullNorEmpty()) {
|
|
return@run Locale(languageTag)
|
|
}*/
|
|
|
|
// user.locale is empty
|
|
val language: String? = System.getProperty("user.language", "pl")
|
|
val region: String? = System.getProperty("user.region")
|
|
val country: String?
|
|
val variant: String?
|
|
// for compatibility, check for old user.region property
|
|
if (region != null) {
|
|
// region can be of form country, country_variant, or _variant
|
|
val i = region.indexOf('_')
|
|
if (i >= 0) {
|
|
country = region.substring(0, i)
|
|
variant = region.substring(i + 1)
|
|
} else {
|
|
country = region
|
|
variant = ""
|
|
}
|
|
} else {
|
|
country = System.getProperty("user.country", "")
|
|
variant = System.getProperty("user.variant", "")
|
|
}
|
|
return@run Locale(language)
|
|
}.let {
|
|
Locale.setDefault(it)
|
|
}
|
|
}
|
|
|
|
fun String.crc16(): Int {
|
|
var crc = 0xFFFF
|
|
for (aBuffer in this) {
|
|
crc = crc.ushr(8) or (crc shl 8) and 0xffff
|
|
crc = crc xor (aBuffer.toInt() and 0xff) // byte to int, trunc sign
|
|
crc = crc xor (crc and 0xff shr 4)
|
|
crc = crc xor (crc shl 12 and 0xffff)
|
|
crc = crc xor (crc and 0xFF shl 5 and 0xffff)
|
|
}
|
|
crc = crc and 0xffff
|
|
return crc + 32768
|
|
}
|
|
|
|
fun String.crc32(): Long {
|
|
val crc = CRC32()
|
|
crc.update(toByteArray())
|
|
return crc.value
|
|
}
|
|
|
|
fun String.hmacSHA1(password: String): String {
|
|
val key = SecretKeySpec(password.toByteArray(), "HmacSHA1")
|
|
|
|
val mac = Mac.getInstance("HmacSHA1").apply {
|
|
init(key)
|
|
update(this@hmacSHA1.toByteArray())
|
|
}
|
|
|
|
return encodeToString(mac.doFinal(), NO_WRAP)
|
|
}
|
|
|
|
fun String.md5(): String {
|
|
val md = MessageDigest.getInstance("MD5")
|
|
return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0')
|
|
}
|
|
|
|
fun String.sha256(): ByteArray {
|
|
val md = MessageDigest.getInstance("SHA-256")
|
|
md.update(toByteArray())
|
|
return md.digest()
|
|
}
|
|
|
|
fun RequestBody.bodyToString(): String {
|
|
val buffer = Buffer()
|
|
writeTo(buffer)
|
|
return buffer.readUtf8()
|
|
}
|
|
|
|
fun Long.formatDate(format: String = "yyyy-MM-dd HH:mm:ss"): String = SimpleDateFormat(format).format(this)
|
|
|
|
fun CharSequence?.asColoredSpannable(colorInt: Int): Spannable {
|
|
val spannable = SpannableString(this)
|
|
spannable.setSpan(ForegroundColorSpan(colorInt), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
return spannable
|
|
}
|
|
fun CharSequence?.asStrikethroughSpannable(): Spannable {
|
|
val spannable = SpannableString(this)
|
|
spannable.setSpan(StrikethroughSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
return spannable
|
|
}
|
|
fun CharSequence?.asItalicSpannable(): Spannable {
|
|
val spannable = SpannableString(this)
|
|
spannable.setSpan(StyleSpan(Typeface.ITALIC), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
return spannable
|
|
}
|
|
fun CharSequence?.asBoldSpannable(): Spannable {
|
|
val spannable = SpannableString(this)
|
|
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
return spannable
|
|
}
|
|
fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false, ignoreDiacritics: Boolean = false): Spannable {
|
|
val spannable = SpannableString(this)
|
|
if (substring == null) {
|
|
spans.forEach {
|
|
spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
}
|
|
}
|
|
else if (substring.isNotEmpty()) {
|
|
val string =
|
|
if (ignoreDiacritics)
|
|
this.cleanDiacritics()
|
|
else this
|
|
|
|
var index = string.indexOf(substring, ignoreCase = ignoreCase)
|
|
.takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase)
|
|
while (index >= 0) {
|
|
spans.forEach {
|
|
spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
}
|
|
index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase)
|
|
.takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase)
|
|
}
|
|
}
|
|
return spannable
|
|
}
|
|
|
|
fun CharSequence.cleanDiacritics(): String {
|
|
val nameClean = StringBuilder()
|
|
forEach {
|
|
val ch = when (it) {
|
|
'ż' -> 'z'
|
|
'ó' -> 'o'
|
|
'ł' -> 'l'
|
|
'ć' -> 'c'
|
|
'ę' -> 'e'
|
|
'ś' -> 's'
|
|
'ą' -> 'a'
|
|
'ź' -> 'z'
|
|
'ń' -> 'n'
|
|
else -> it
|
|
}
|
|
nameClean.append(ch)
|
|
}
|
|
return nameClean.toString()
|
|
}
|
|
|
|
/**
|
|
* Returns a new read-only list only of those given elements, that are not empty.
|
|
* Applies for CharSequence and descendants.
|
|
*/
|
|
fun <T : CharSequence> listOfNotEmpty(vararg elements: T): List<T> = elements.filterNot { it.isEmpty() }
|
|
|
|
fun List<CharSequence?>.concat(delimiter: CharSequence? = null): CharSequence {
|
|
if (this.isEmpty()) {
|
|
return ""
|
|
}
|
|
|
|
if (this.size == 1) {
|
|
return this[0] ?: ""
|
|
}
|
|
|
|
var spanned = delimiter is Spanned
|
|
if (!spanned) {
|
|
for (piece in this) {
|
|
if (piece is Spanned) {
|
|
spanned = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var first = true
|
|
if (spanned) {
|
|
val ssb = SpannableStringBuilder()
|
|
for (piece in this) {
|
|
if (piece == null)
|
|
continue
|
|
if (!first && delimiter != null)
|
|
ssb.append(delimiter)
|
|
first = false
|
|
ssb.append(piece)
|
|
}
|
|
return SpannedString(ssb)
|
|
} else {
|
|
val sb = StringBuilder()
|
|
for (piece in this) {
|
|
if (piece == null)
|
|
continue
|
|
if (!first && delimiter != null)
|
|
sb.append(delimiter)
|
|
first = false
|
|
sb.append(piece)
|
|
}
|
|
return sb.toString()
|
|
}
|
|
}
|
|
|
|
fun TextView.setText(@StringRes resid: Int, vararg formatArgs: Any) {
|
|
text = context.getString(resid, *formatArgs)
|
|
}
|
|
|
|
fun MaterialAlertDialogBuilder.setTitle(@StringRes resid: Int, vararg formatArgs: Any): MaterialAlertDialogBuilder {
|
|
setTitle(context.getString(resid, *formatArgs))
|
|
return this
|
|
}
|
|
|
|
fun MaterialAlertDialogBuilder.setMessage(@StringRes resid: Int, vararg formatArgs: Any): MaterialAlertDialogBuilder {
|
|
setMessage(context.getString(resid, *formatArgs))
|
|
return this
|
|
}
|
|
|
|
fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
|
|
return JsonObject().apply {
|
|
for (property in properties) {
|
|
when (property.second) {
|
|
is JsonElement -> add(property.first, property.second as JsonElement?)
|
|
is String -> addProperty(property.first, property.second as String?)
|
|
is Char -> addProperty(property.first, property.second as Char?)
|
|
is Number -> addProperty(property.first, property.second as Number?)
|
|
is Boolean -> addProperty(property.first, property.second as Boolean?)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun JsonArray(vararg properties: Any?): JsonArray {
|
|
return JsonArray().apply {
|
|
for (property in properties) {
|
|
when (property) {
|
|
is JsonElement -> add(property as JsonElement?)
|
|
is String -> add(property as String?)
|
|
is Char -> add(property as Char?)
|
|
is Number -> add(property as Number?)
|
|
is Boolean -> add(property as Boolean?)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
|
|
return Bundle().apply {
|
|
for (property in properties) {
|
|
when (property.second) {
|
|
is String -> putString(property.first, property.second as String?)
|
|
is Char -> putChar(property.first, property.second as Char)
|
|
is Int -> putInt(property.first, property.second as Int)
|
|
is Long -> putLong(property.first, property.second as Long)
|
|
is Float -> putFloat(property.first, property.second as Float)
|
|
is Short -> putShort(property.first, property.second as Short)
|
|
is Double -> putDouble(property.first, property.second as Double)
|
|
is Boolean -> putBoolean(property.first, property.second as Boolean)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fun Intent(action: String? = null, vararg properties: Pair<String, Any?>): Intent {
|
|
return Intent(action).putExtras(Bundle(*properties))
|
|
}
|
|
fun Intent(packageContext: Context, cls: Class<*>, vararg properties: Pair<String, Any?>): Intent {
|
|
return Intent(packageContext, cls).putExtras(Bundle(*properties))
|
|
}
|
|
|
|
fun Bundle.toJsonObject(): JsonObject {
|
|
val json = JsonObject()
|
|
keySet()?.forEach { key ->
|
|
get(key)?.let {
|
|
when (it) {
|
|
is String -> json.addProperty(key, it)
|
|
is Char -> json.addProperty(key, it)
|
|
is Int -> json.addProperty(key, it)
|
|
is Long -> json.addProperty(key, it)
|
|
is Float -> json.addProperty(key, it)
|
|
is Short -> json.addProperty(key, it)
|
|
is Double -> json.addProperty(key, it)
|
|
is Boolean -> json.addProperty(key, it)
|
|
is Bundle -> json.add(key, it.toJsonObject())
|
|
else -> json.addProperty(key, it.toString())
|
|
}
|
|
}
|
|
}
|
|
return json
|
|
}
|
|
fun Intent.toJsonObject() = extras?.toJsonObject()
|
|
|
|
fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0
|
|
fun JsonArray.isEmpty(): Boolean = this.size() == 0
|
|
operator fun JsonArray.plusAssign(o: JsonElement) = this.add(o)
|
|
operator fun JsonArray.plusAssign(o: String) = this.add(o)
|
|
operator fun JsonArray.plusAssign(o: Char) = this.add(o)
|
|
operator fun JsonArray.plusAssign(o: Number) = this.add(o)
|
|
operator fun JsonArray.plusAssign(o: Boolean) = this.add(o)
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
inline fun <T : View> T.onClick(crossinline onClickListener: (v: T) -> Unit) {
|
|
setOnClickListener { v: View ->
|
|
onClickListener(v as T)
|
|
}
|
|
}
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
inline fun <T : View> T.onLongClick(crossinline onLongClickListener: (v: T) -> Boolean) {
|
|
setOnLongClickListener { v: View ->
|
|
onLongClickListener(v as T)
|
|
}
|
|
}
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
inline fun <T : CompoundButton> T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) {
|
|
setOnCheckedChangeListener { buttonView, isChecked ->
|
|
onChangeListener(buttonView as T, isChecked)
|
|
}
|
|
}
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
inline fun <T : MaterialButton> T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) {
|
|
clearOnCheckedChangeListeners()
|
|
addOnCheckedChangeListener { buttonView, isChecked ->
|
|
onChangeListener(buttonView as T, isChecked)
|
|
}
|
|
}
|
|
|
|
fun View.attachToastHint(stringRes: Int) = onLongClick {
|
|
Toast.makeText(it.context, stringRes, Toast.LENGTH_SHORT).show()
|
|
true
|
|
}
|
|
|
|
fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
|
|
observe(lifecycleOwner, object : Observer<T> {
|
|
override fun onChanged(t: T?) {
|
|
observer.onChanged(t)
|
|
removeObserver(this)
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Convert a value in dp to pixels.
|
|
*/
|
|
val Int.dp: Int
|
|
get() = (this * Resources.getSystem().displayMetrics.density).toInt()
|
|
/**
|
|
* Convert a value in pixels to dp.
|
|
*/
|
|
val Int.px: Int
|
|
get() = (this / Resources.getSystem().displayMetrics.density).toInt()
|
|
|
|
@ColorInt
|
|
fun @receiver:AttrRes Int.resolveAttr(context: Context?): Int {
|
|
val typedValue = TypedValue()
|
|
context?.theme?.resolveAttribute(this, typedValue, true)
|
|
return typedValue.data
|
|
}
|
|
@ColorInt
|
|
fun @receiver:ColorRes Int.resolveColor(context: Context): Int {
|
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
context.resources.getColor(this, context.theme)
|
|
}
|
|
else {
|
|
context.resources.getColor(this)
|
|
}
|
|
}
|
|
fun @receiver:DrawableRes Int.resolveDrawable(context: Context): Drawable {
|
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
context.resources.getDrawable(this, context.theme)
|
|
}
|
|
else {
|
|
context.resources.getDrawable(this)
|
|
}
|
|
}
|
|
|
|
fun View.findParentById(targetId: Int): View? {
|
|
if (id == targetId) {
|
|
return this
|
|
}
|
|
val viewParent = this.parent ?: return null
|
|
if (viewParent is View) {
|
|
return viewParent.findParentById(targetId)
|
|
}
|
|
return null
|
|
}
|
|
|
|
fun CoroutineScope.startCoroutineTimer(delayMillis: Long = 0, repeatMillis: Long = 0, action: suspend CoroutineScope.() -> Unit) = launch {
|
|
delay(delayMillis)
|
|
if (repeatMillis > 0) {
|
|
while (true) {
|
|
action()
|
|
delay(repeatMillis)
|
|
}
|
|
} else {
|
|
action()
|
|
}
|
|
}
|
|
|
|
operator fun Time?.compareTo(other: Time?): Int {
|
|
if (this == null && other == null)
|
|
return 0
|
|
if (this == null)
|
|
return -1
|
|
if (other == null)
|
|
return 1
|
|
return this.compareTo(other)
|
|
}
|
|
|
|
operator fun StringBuilder.plusAssign(str: String?) {
|
|
this.append(str)
|
|
}
|
|
|
|
fun Context.timeTill(time: Int, delimiter: String = " ", countInSeconds: Boolean = false): String {
|
|
val parts = mutableListOf<Pair<Int, Int>>()
|
|
|
|
val hours = time / 3600
|
|
val minutes = (time - hours*3600) / 60
|
|
val seconds = time - minutes*60 - hours*3600
|
|
|
|
if (!countInSeconds) {
|
|
var prefixAdded = false
|
|
if (hours > 0) {
|
|
if (!prefixAdded) parts += R.plurals.time_till_text to hours
|
|
prefixAdded = true
|
|
parts += R.plurals.time_till_hours to hours
|
|
}
|
|
if (minutes > 0) {
|
|
if (!prefixAdded) parts += R.plurals.time_till_text to minutes
|
|
prefixAdded = true
|
|
parts += R.plurals.time_till_minutes to minutes
|
|
}
|
|
if (hours == 0 && minutes < 10) {
|
|
if (!prefixAdded) parts += R.plurals.time_till_text to seconds
|
|
prefixAdded = true
|
|
parts += R.plurals.time_till_seconds to seconds
|
|
}
|
|
} else {
|
|
parts += R.plurals.time_till_text to time
|
|
parts += R.plurals.time_till_seconds to time
|
|
}
|
|
|
|
return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) }
|
|
}
|
|
|
|
fun Context.timeLeft(time: Int, delimiter: String = " ", countInSeconds: Boolean = false): String {
|
|
val parts = mutableListOf<Pair<Int, Int>>()
|
|
|
|
val hours = time / 3600
|
|
val minutes = (time - hours*3600) / 60
|
|
val seconds = time - minutes*60 - hours*3600
|
|
|
|
if (!countInSeconds) {
|
|
var prefixAdded = false
|
|
if (hours > 0) {
|
|
if (!prefixAdded) parts += R.plurals.time_left_text to hours
|
|
prefixAdded = true
|
|
parts += R.plurals.time_left_hours to hours
|
|
}
|
|
if (minutes > 0) {
|
|
if (!prefixAdded) parts += R.plurals.time_left_text to minutes
|
|
prefixAdded = true
|
|
parts += R.plurals.time_left_minutes to minutes
|
|
}
|
|
if (hours == 0 && minutes < 10) {
|
|
if (!prefixAdded) parts += R.plurals.time_left_text to seconds
|
|
prefixAdded = true
|
|
parts += R.plurals.time_left_seconds to seconds
|
|
}
|
|
} else {
|
|
parts += R.plurals.time_left_text to time
|
|
parts += R.plurals.time_left_seconds to time
|
|
}
|
|
|
|
return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) }
|
|
}
|
|
|
|
inline fun <reified T> Any?.instanceOfOrNull(): T? {
|
|
return when (this) {
|
|
is T -> this
|
|
else -> null
|
|
}
|
|
}
|
|
|
|
fun Drawable.setTintColor(color: Int): Drawable {
|
|
colorFilter = PorterDuffColorFilter(
|
|
color,
|
|
PorterDuff.Mode.SRC_ATOP
|
|
)
|
|
return this
|
|
}
|
|
|
|
inline fun <T> List<T>.ifNotEmpty(block: (List<T>) -> Unit) {
|
|
if (!isEmpty())
|
|
block(this)
|
|
}
|
|
|
|
val String.firstLettersName: String
|
|
get() {
|
|
var nameShort = ""
|
|
this.split(" ").forEach {
|
|
if (it.isBlank())
|
|
return@forEach
|
|
nameShort += it[0].toLowerCase()
|
|
}
|
|
return nameShort
|
|
}
|
|
|
|
val Throwable.stackTraceString: String
|
|
get() {
|
|
val sw = StringWriter()
|
|
printStackTrace(PrintWriter(sw))
|
|
return sw.toString()
|
|
}
|
|
|
|
inline fun <T> LongSparseArray<T>.filter(predicate: (T) -> Boolean): List<T> {
|
|
val destination = ArrayList<T>()
|
|
this.forEach { _, element -> if (predicate(element)) destination.add(element) }
|
|
return destination
|
|
}
|
|
|
|
fun CharSequence.replace(oldValue: String, newValue: CharSequence, ignoreCase: Boolean = false): CharSequence =
|
|
splitToSequence(oldValue, ignoreCase = ignoreCase).toList().concat(newValue)
|
|
|
|
fun Int.toColorStateList(): ColorStateList {
|
|
val states = arrayOf(
|
|
intArrayOf( android.R.attr.state_enabled ),
|
|
intArrayOf(-android.R.attr.state_enabled ),
|
|
intArrayOf(-android.R.attr.state_checked ),
|
|
intArrayOf( android.R.attr.state_pressed )
|
|
)
|
|
|
|
val colors = intArrayOf(
|
|
this,
|
|
this,
|
|
this,
|
|
this
|
|
)
|
|
|
|
return ColorStateList(states, colors)
|
|
}
|
|
|
|
fun SpannableStringBuilder.appendText(text: CharSequence): SpannableStringBuilder {
|
|
append(text)
|
|
return this
|
|
}
|
|
fun SpannableStringBuilder.appendSpan(text: CharSequence, what: Any, flags: Int): SpannableStringBuilder {
|
|
val start: Int = length
|
|
append(text)
|
|
setSpan(what, start, length, flags)
|
|
return this
|
|
}
|
|
|
|
fun joinNotNullStrings(delimiter: String = "", vararg parts: String?): String {
|
|
var first = true
|
|
val sb = StringBuilder()
|
|
for (part in parts) {
|
|
if (part == null)
|
|
continue
|
|
if (!first)
|
|
sb += delimiter
|
|
first = false
|
|
sb += part
|
|
}
|
|
return sb.toString()
|
|
}
|
|
|
|
fun String.notEmptyOrNull(): String? {
|
|
return if (isEmpty())
|
|
null
|
|
else
|
|
this
|
|
}
|
|
|
|
fun String.base64Encode(): String {
|
|
return encodeToString(toByteArray(), NO_WRAP)
|
|
}
|
|
fun ByteArray.base64Encode(): String {
|
|
return encodeToString(this, NO_WRAP)
|
|
}
|
|
fun String.base64Decode(): ByteArray {
|
|
return Base64.decode(this, Base64.DEFAULT)
|
|
}
|
|
fun String.base64DecodeToString(): String {
|
|
return Base64.decode(this, Base64.DEFAULT).toString(Charset.defaultCharset())
|
|
}
|
|
|
|
fun CheckBox.trigger() { isChecked = !isChecked }
|
|
|
|
fun Context.plural(@PluralsRes resId: Int, value: Int): String = resources.getQuantityString(resId, value, value)
|
|
|
|
fun Context.getNotificationTitle(type: Int): String {
|
|
return getString(when (type) {
|
|
Notification.TYPE_UPDATE -> R.string.notification_type_update
|
|
Notification.TYPE_ERROR -> R.string.notification_type_error
|
|
Notification.TYPE_TIMETABLE_CHANGED -> R.string.notification_type_timetable_change
|
|
Notification.TYPE_TIMETABLE_LESSON_CHANGE -> R.string.notification_type_timetable_lesson_change
|
|
Notification.TYPE_NEW_GRADE -> R.string.notification_type_new_grade
|
|
Notification.TYPE_NEW_EVENT -> R.string.notification_type_new_event
|
|
Notification.TYPE_NEW_HOMEWORK -> R.string.notification_type_new_homework
|
|
Notification.TYPE_NEW_SHARED_EVENT -> R.string.notification_type_new_shared_event
|
|
Notification.TYPE_NEW_SHARED_HOMEWORK -> R.string.notification_type_new_shared_homework
|
|
Notification.TYPE_REMOVED_SHARED_EVENT -> R.string.notification_type_removed_shared_event
|
|
Notification.TYPE_NEW_MESSAGE -> R.string.notification_type_new_message
|
|
Notification.TYPE_NEW_NOTICE -> R.string.notification_type_notice
|
|
Notification.TYPE_NEW_ATTENDANCE -> R.string.notification_type_attendance
|
|
Notification.TYPE_SERVER_MESSAGE -> R.string.notification_type_server_message
|
|
Notification.TYPE_LUCKY_NUMBER -> R.string.notification_type_lucky_number
|
|
Notification.TYPE_FEEDBACK_MESSAGE -> R.string.notification_type_feedback_message
|
|
Notification.TYPE_NEW_ANNOUNCEMENT -> R.string.notification_type_new_announcement
|
|
Notification.TYPE_AUTO_ARCHIVING -> R.string.notification_type_auto_archiving
|
|
Notification.TYPE_TEACHER_ABSENCE -> R.string.notification_type_new_teacher_absence
|
|
Notification.TYPE_GENERAL -> R.string.notification_type_general
|
|
else -> R.string.notification_type_general
|
|
})
|
|
}
|
|
|
|
fun Cursor?.getString(columnName: String) = this?.getStringOrNull(getColumnIndex(columnName))
|
|
fun Cursor?.getInt(columnName: String) = this?.getIntOrNull(getColumnIndex(columnName))
|
|
fun Cursor?.getLong(columnName: String) = this?.getLongOrNull(getColumnIndex(columnName))
|
|
|
|
fun OkHttpClient.Builder.installHttpsSupport(context: Context) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
try {
|
|
try {
|
|
ProviderInstaller.installIfNeeded(context)
|
|
} catch (e: Exception) {
|
|
Log.e("OkHttpTLSCompat", "Play Services not found or outdated")
|
|
|
|
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
|
trustManagerFactory.init(null as KeyStore?)
|
|
|
|
val x509TrustManager = trustManagerFactory.trustManagers.singleOrNull { it is X509TrustManager } as X509TrustManager?
|
|
?: return
|
|
|
|
val sc = SSLContext.getInstance("TLSv1.2")
|
|
sc.init(null, null, null)
|
|
sslSocketFactory(TLSSocketFactory(sc.socketFactory), x509TrustManager)
|
|
val cs: ConnectionSpec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
|
.tlsVersions(TlsVersion.TLS_1_0)
|
|
.tlsVersions(TlsVersion.TLS_1_1)
|
|
.tlsVersions(TlsVersion.TLS_1_2)
|
|
.build()
|
|
val specs: MutableList<ConnectionSpec> = ArrayList()
|
|
specs.add(cs)
|
|
specs.add(ConnectionSpec.COMPATIBLE_TLS)
|
|
specs.add(ConnectionSpec.CLEARTEXT)
|
|
connectionSpecs(specs)
|
|
}
|
|
} catch (exc: Exception) {
|
|
Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun CharSequence.containsAll(list: List<CharSequence>, ignoreCase: Boolean = false): Boolean {
|
|
for (i in list) {
|
|
if (!contains(i, ignoreCase))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
inline fun RadioButton.setOnSelectedListener(crossinline listener: (buttonView: CompoundButton) -> Unit)
|
|
= setOnCheckedChangeListener { buttonView, isChecked -> if (isChecked) listener(buttonView) }
|
|
|
|
fun Response.toErrorCode() = when (this.code()) {
|
|
400 -> ERROR_REQUEST_HTTP_400
|
|
401 -> ERROR_REQUEST_HTTP_401
|
|
403 -> ERROR_REQUEST_HTTP_403
|
|
404 -> ERROR_REQUEST_HTTP_404
|
|
405 -> ERROR_REQUEST_HTTP_405
|
|
410 -> ERROR_REQUEST_HTTP_410
|
|
424 -> ERROR_REQUEST_HTTP_424
|
|
500 -> ERROR_REQUEST_HTTP_500
|
|
503 -> ERROR_REQUEST_HTTP_503
|
|
else -> null
|
|
}
|
|
fun Throwable.toErrorCode() = when (this) {
|
|
is UnknownHostException -> ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND
|
|
is SSLException -> ERROR_REQUEST_FAILURE_SSL_ERROR
|
|
is SocketTimeoutException -> ERROR_REQUEST_FAILURE_TIMEOUT
|
|
is InterruptedIOException, is ConnectException -> ERROR_REQUEST_FAILURE_NO_INTERNET
|
|
is SzkolnyApiException -> this.error?.toErrorCode()
|
|
else -> null
|
|
}
|
|
private fun ApiResponse.Error.toErrorCode() = when (this.code) {
|
|
else -> ERROR_API_EXCEPTION
|
|
}
|
|
fun Throwable.toApiError(tag: String) = ApiError.fromThrowable(tag, this)
|
|
|
|
inline fun <A, B, R> ifNotNull(a: A?, b: B?, code: (A, B) -> R): R? {
|
|
if (a != null && b != null) {
|
|
return code(a, b)
|
|
}
|
|
return null
|
|
}
|
|
|
|
@kotlin.jvm.JvmName("averageOrNullOfInt")
|
|
fun Iterable<Int>.averageOrNull() = this.average().let { if (it.isNaN()) null else it }
|
|
@kotlin.jvm.JvmName("averageOrNullOfFloat")
|
|
fun Iterable<Float>.averageOrNull() = this.average().let { if (it.isNaN()) null else it }
|
|
|
|
fun String.copyToClipboard(context: Context) {
|
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
val clipData = ClipData.newPlainText("Tekst", this)
|
|
clipboard.primaryClip = clipData
|
|
}
|
|
|
|
fun TextView.getTextPosition(range: IntRange): Rect {
|
|
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
|
|
|
// Initialize global value
|
|
var parentTextViewRect = Rect()
|
|
|
|
// Initialize values for the computing of clickedText position
|
|
//val completeText = parentTextView.text as SpannableString
|
|
val textViewLayout = this.layout
|
|
|
|
val startOffsetOfClickedText = range.first//completeText.getSpanStart(clickedText)
|
|
val endOffsetOfClickedText = range.last//completeText.getSpanEnd(clickedText)
|
|
var startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(startOffsetOfClickedText)
|
|
var endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(endOffsetOfClickedText)
|
|
|
|
// Get the rectangle of the clicked text
|
|
val currentLineStartOffset = textViewLayout.getLineForOffset(startOffsetOfClickedText)
|
|
val currentLineEndOffset = textViewLayout.getLineForOffset(endOffsetOfClickedText)
|
|
val keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset
|
|
textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect)
|
|
|
|
// Update the rectangle position to his real position on screen
|
|
val parentTextViewLocation = intArrayOf(0, 0)
|
|
this.getLocationOnScreen(parentTextViewLocation)
|
|
|
|
val parentTextViewTopAndBottomOffset = (parentTextViewLocation[1] - this.scrollY + this.compoundPaddingTop)
|
|
parentTextViewRect.top += parentTextViewTopAndBottomOffset
|
|
parentTextViewRect.bottom += parentTextViewTopAndBottomOffset
|
|
|
|
// In the case of multi line text, we have to choose what rectangle take
|
|
if (keywordIsInMultiLine) {
|
|
val screenHeight = windowManager.defaultDisplay.height
|
|
val dyTop = parentTextViewRect.top
|
|
val dyBottom = screenHeight - parentTextViewRect.bottom
|
|
val onTop = dyTop > dyBottom
|
|
|
|
if (onTop) {
|
|
endXCoordinatesOfClickedText = textViewLayout.getLineRight(currentLineStartOffset);
|
|
} else {
|
|
parentTextViewRect = Rect()
|
|
textViewLayout.getLineBounds(currentLineEndOffset, parentTextViewRect);
|
|
parentTextViewRect.top += parentTextViewTopAndBottomOffset;
|
|
parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
|
|
startXCoordinatesOfClickedText = textViewLayout.getLineLeft(currentLineEndOffset);
|
|
}
|
|
}
|
|
|
|
parentTextViewRect.left += (
|
|
parentTextViewLocation[0] +
|
|
startXCoordinatesOfClickedText +
|
|
this.compoundPaddingLeft -
|
|
this.scrollX
|
|
).toInt()
|
|
parentTextViewRect.right = (
|
|
parentTextViewRect.left +
|
|
endXCoordinatesOfClickedText -
|
|
startXCoordinatesOfClickedText
|
|
).toInt()
|
|
|
|
return parentTextViewRect
|
|
}
|
|
|
|
inline fun ViewPager.addOnPageSelectedListener(crossinline block: (position: Int) -> Unit) = addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
|
|
override fun onPageScrollStateChanged(state: Int) {}
|
|
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
|
|
override fun onPageSelected(position: Int) { block(position) }
|
|
})
|
|
|
|
val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener
|
|
get() = object : RecyclerView.OnScrollListener() {
|
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
if (recyclerView.canScrollVertically(-1))
|
|
this@onScrollListener.isEnabled = false
|
|
if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE)
|
|
this@onScrollListener.isEnabled = true
|
|
}
|
|
}
|
|
|
|
operator fun <K, V> Iterable<Pair<K, V>>.get(key: K): V? {
|
|
return firstOrNull { it.first == key }?.second
|
|
}
|
|
|
|
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
|