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.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.byId(id: Long) = firstOrNull { it.id == id } fun List.byNameFirstLast(nameFirstLast: String) = firstOrNull { it.name + " " + it.surname == nameFirstLast } fun List.byNameLastFirst(nameLastFirst: String) = firstOrNull { it.surname + " " + it.name == nameLastFirst } fun List.byNameFDotLast(nameFDotLast: String) = firstOrNull { it.name + "." + it.surname == nameFDotLast } fun List.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 Collection?.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 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? { 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.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 -> -1 }.toInt() } fun List.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 LongSparseArray.values(): List { val result = mutableListOf() forEach { _, value -> result += value } return result } fun SparseArray<*>.keys(): List { val result = mutableListOf() forEach { key, _ -> result += key } return result } fun SparseArray.values(): List { val result = mutableListOf() forEach { _, value -> result += value } return result } fun SparseIntArray.keys(): List { val result = mutableListOf() forEach { key, _ -> result += key } return result } fun SparseIntArray.values(): List { val result = mutableListOf() forEach { _, value -> result += value } return result } fun List>.keys(): List { val result = mutableListOf() forEach { pair -> result += pair.first } return result } fun List>.values(): List { val result = mutableListOf() forEach { pair -> result += pair.second } return result } fun List.toSparseArray(destination: SparseArray, key: (T) -> Int) { forEach { destination.put(key(it), it) } } fun List.toSparseArray(destination: LongSparseArray, key: (T) -> Long) { forEach { destination.put(key(it), it) } } fun List.toSparseArray(key: (T) -> Int): SparseArray { val result = SparseArray() toSparseArray(result, key) return result } fun List.toSparseArray(key: (T) -> Long): LongSparseArray { val result = LongSparseArray() toSparseArray(result, key) return result } fun SparseArray.singleOrNull(predicate: (T) -> Boolean): T? { forEach { _, value -> if (predicate(value)) return value } return null } fun LongSparseArray.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.getById(id: Long): Team? { return singleOrNull { it.id == id } } fun LongSparseArray.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 listOfNotEmpty(vararg elements: T): List = elements.filterNot { it.isEmpty() } fun List.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 JsonObject(vararg properties: Pair): 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): 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): Intent { return Intent(action).putExtras(Bundle(*properties)) } fun Intent(packageContext: Context, cls: Class<*>, vararg properties: Pair): 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.onClick(crossinline onClickListener: (v: T) -> Unit) { setOnClickListener { v: View -> onClickListener(v as T) } } @Suppress("UNCHECKED_CAST") inline fun T.onLongClick(crossinline onLongClickListener: (v: T) -> Boolean) { setOnLongClickListener { v: View -> onLongClickListener(v as T) } } @Suppress("UNCHECKED_CAST") inline fun T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { setOnCheckedChangeListener { buttonView, isChecked -> onChangeListener(buttonView as T, isChecked) } } @Suppress("UNCHECKED_CAST") inline fun 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 LiveData.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer) { observe(lifecycleOwner, object : Observer { 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>() 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>() 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 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 List.ifNotEmpty(block: (List) -> 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 LongSparseArray.filter(predicate: (T) -> Boolean): List { val destination = ArrayList() 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 = 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, 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 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.averageOrNull() = this.average().let { if (it.isNaN()) null else it } @kotlin.jvm.JvmName("averageOrNullOfFloat") fun Iterable.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 Iterable>.get(key: K): V? { return firstOrNull { it.first == key }?.second } fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }