package pl.szczodrzynski.edziennik import android.Manifest import android.app.Activity import android.content.Context import android.content.pm.PackageManager import android.content.res.ColorStateList import android.content.res.Resources import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter 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.Base64 import android.util.Base64.NO_WRAP import android.util.Base64.encodeToString import android.util.LongSparseArray import android.util.SparseArray import android.util.TypedValue import android.view.View import android.widget.CompoundButton import android.widget.TextView import androidx.annotation.* import androidx.core.app.ActivityCompat import androidx.core.util.forEach import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import im.wangchao.mhttp.Response import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import okhttp3.RequestBody import okio.Buffer import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher import pl.szczodrzynski.edziennik.data.db.modules.teams.Team import pl.szczodrzynski.edziennik.utils.models.Time import java.io.PrintWriter import java.io.StringWriter import java.math.BigInteger import java.nio.charset.Charset 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 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.isJsonNull) null else it.asJsonObject } fun JsonObject?.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonNull) null else it.asJsonArray } 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.isJsonNull) defaultValue else it.asJsonObject } ?: defaultValue fun JsonObject?.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonNull) defaultValue else it.asJsonArray } ?: defaultValue 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.map { it.asJsonObject } fun CharSequence?.isNotNullNorEmpty(): Boolean { return this != null && this.isNotEmpty() } 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 } /** * ` 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.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 Activity.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) baseContext.resources.updateConfiguration(configuration, baseContext.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): Spannable { val spannable = SpannableString(this) if (substring == null) { spans.forEach { spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } else if (substring.isNotEmpty()) { var index = indexOf(substring, ignoreCase = ignoreCase) while (index >= 0) { spans.forEach { spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } index = indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase); } } return spannable } /** * 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 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.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { setOnCheckedChangeListener { buttonView, isChecked -> onChangeListener(buttonView as T, isChecked) } } 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: () -> 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()) }