2021-11-27 22:18:09 +01:00
|
|
|
package io.heckel.ntfy.util
|
|
|
|
|
|
|
|
import android.animation.ArgbEvaluator
|
|
|
|
import android.animation.ValueAnimator
|
2022-01-10 04:08:29 +01:00
|
|
|
import android.content.Context
|
|
|
|
import android.net.Uri
|
2022-01-11 23:00:18 +01:00
|
|
|
import android.provider.OpenableColumns
|
2021-11-27 22:18:09 +01:00
|
|
|
import android.view.Window
|
|
|
|
import io.heckel.ntfy.data.Notification
|
2022-01-10 04:08:29 +01:00
|
|
|
import io.heckel.ntfy.data.PROGRESS_NONE
|
2021-11-27 22:18:09 +01:00
|
|
|
import io.heckel.ntfy.data.Subscription
|
2021-12-30 01:05:32 +01:00
|
|
|
import java.security.SecureRandom
|
2021-11-27 22:18:09 +01:00
|
|
|
import java.text.DateFormat
|
2022-01-09 03:32:43 +01:00
|
|
|
import java.text.StringCharacterIterator
|
2021-11-27 22:18:09 +01:00
|
|
|
import java.util.*
|
2022-01-12 01:37:34 +01:00
|
|
|
import kotlin.math.abs
|
2021-11-27 22:18:09 +01:00
|
|
|
|
|
|
|
fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
|
2021-12-29 20:33:17 +01:00
|
|
|
fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // UnifiedPush
|
2021-11-27 22:18:09 +01:00
|
|
|
fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
|
2022-01-02 01:37:09 +01:00
|
|
|
fun topicUrlJsonPoll(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?poll=1&since=$since"
|
2021-11-27 22:18:09 +01:00
|
|
|
fun topicShortUrl(baseUrl: String, topic: String) =
|
|
|
|
topicUrl(baseUrl, topic)
|
|
|
|
.replace("http://", "")
|
|
|
|
.replace("https://", "")
|
|
|
|
|
|
|
|
fun formatDateShort(timestampSecs: Long): String {
|
2022-01-09 03:32:43 +01:00
|
|
|
val date = Date(timestampSecs*1000)
|
|
|
|
return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date)
|
2021-11-27 22:18:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fun toPriority(priority: Int?): Int {
|
|
|
|
if (priority != null && (1..5).contains(priority)) return priority
|
|
|
|
else return 3
|
|
|
|
}
|
|
|
|
|
2022-01-01 16:56:18 +01:00
|
|
|
fun toPriorityString(priority: Int): String {
|
|
|
|
return when (priority) {
|
|
|
|
1 -> "min"
|
|
|
|
2 -> "low"
|
|
|
|
3 -> "default"
|
|
|
|
4 -> "high"
|
|
|
|
5 -> "max"
|
|
|
|
else -> "default"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-27 22:18:09 +01:00
|
|
|
fun joinTags(tags: List<String>?): String {
|
|
|
|
return tags?.joinToString(",") ?: ""
|
2021-12-11 21:09:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fun joinTagsMap(tags: List<String>?): String {
|
2021-12-14 05:38:23 +01:00
|
|
|
return tags?.mapIndexed { i, tag -> "${i+1}=${tag}" }?.joinToString(",") ?: ""
|
2021-11-27 22:18:09 +01:00
|
|
|
}
|
|
|
|
|
2021-11-29 01:28:58 +01:00
|
|
|
fun splitTags(tags: String?): List<String> {
|
|
|
|
return if (tags == null || tags == "") {
|
|
|
|
emptyList()
|
|
|
|
} else {
|
|
|
|
tags.split(",")
|
|
|
|
}
|
2021-11-27 22:18:09 +01:00
|
|
|
}
|
|
|
|
|
2021-11-29 01:28:58 +01:00
|
|
|
fun toEmojis(tags: List<String>): List<String> {
|
|
|
|
return tags.mapNotNull { tag -> toEmoji(tag) }
|
|
|
|
}
|
|
|
|
|
|
|
|
fun toEmoji(tag: String): String? {
|
2021-11-29 03:47:00 +01:00
|
|
|
return EmojiManager.getForAlias(tag)?.unicode
|
2021-11-27 22:18:09 +01:00
|
|
|
}
|
|
|
|
|
2021-11-29 01:28:58 +01:00
|
|
|
fun unmatchedTags(tags: List<String>): List<String> {
|
|
|
|
return tags.filter { tag -> toEmoji(tag) == null }
|
|
|
|
}
|
|
|
|
|
2021-11-27 22:18:09 +01:00
|
|
|
/**
|
|
|
|
* Prepend tags/emojis to message, but only if there is a non-empty title.
|
|
|
|
* Otherwise the tags will be prepended to the title.
|
|
|
|
*/
|
|
|
|
fun formatMessage(notification: Notification): String {
|
|
|
|
return if (notification.title != "") {
|
|
|
|
notification.message
|
|
|
|
} else {
|
2021-11-29 01:28:58 +01:00
|
|
|
val emojis = toEmojis(splitTags(notification.tags))
|
2021-11-27 22:18:09 +01:00
|
|
|
if (emojis.isEmpty()) {
|
|
|
|
notification.message
|
|
|
|
} else {
|
|
|
|
emojis.joinToString("") + " " + notification.message
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* See above; prepend emojis to title if the title is non-empty.
|
|
|
|
* Otherwise, they are prepended to the message.
|
|
|
|
*/
|
|
|
|
fun formatTitle(subscription: Subscription, notification: Notification): String {
|
|
|
|
return if (notification.title != "") {
|
|
|
|
formatTitle(notification)
|
|
|
|
} else {
|
|
|
|
topicShortUrl(subscription.baseUrl, subscription.topic)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun formatTitle(notification: Notification): String {
|
2021-11-29 01:28:58 +01:00
|
|
|
val emojis = toEmojis(splitTags(notification.tags))
|
2021-11-27 22:18:09 +01:00
|
|
|
return if (emojis.isEmpty()) {
|
|
|
|
notification.title
|
|
|
|
} else {
|
|
|
|
emojis.joinToString("") + " " + notification.title
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-10 04:08:29 +01:00
|
|
|
// Checks in the most horrible way if a content URI exists; I couldn't find a better way
|
2022-01-11 23:00:18 +01:00
|
|
|
fun fileExists(context: Context, uri: String?): Boolean {
|
|
|
|
if (uri == null) return false
|
2022-01-10 04:08:29 +01:00
|
|
|
val resolver = context.applicationContext.contentResolver
|
|
|
|
return try {
|
|
|
|
val fileIS = resolver.openInputStream(Uri.parse(uri))
|
|
|
|
fileIS?.close()
|
|
|
|
true
|
|
|
|
} catch (_: Exception) {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-11 23:00:18 +01:00
|
|
|
// Queries the filename of a content URI
|
|
|
|
fun queryFilename(context: Context, contentUri: String?, fallbackName: String): String {
|
|
|
|
if (contentUri == null) {
|
|
|
|
return fallbackName
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
val resolver = context.applicationContext.contentResolver
|
|
|
|
val cursor = resolver.query(Uri.parse(contentUri), null, null, null, null) ?: return fallbackName
|
|
|
|
return cursor.use { c ->
|
|
|
|
val nameIndex = c.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
|
|
|
|
c.moveToFirst()
|
|
|
|
c.getString(nameIndex)
|
|
|
|
}
|
|
|
|
} catch (_: Exception) {
|
|
|
|
return fallbackName
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-27 22:18:09 +01:00
|
|
|
// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
|
|
|
|
fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
|
|
|
|
val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
|
|
|
|
statusBarColorAnimation.addUpdateListener { animator ->
|
|
|
|
val color = animator.animatedValue as Int
|
|
|
|
window.statusBarColor = color
|
|
|
|
}
|
|
|
|
statusBarColorAnimation.start()
|
|
|
|
}
|
2021-12-30 01:05:32 +01:00
|
|
|
|
2021-12-31 15:30:49 +01:00
|
|
|
// Generates a (cryptographically secure) random string of a certain length
|
2021-12-30 01:05:32 +01:00
|
|
|
fun randomString(len: Int): String {
|
|
|
|
val random = SecureRandom()
|
|
|
|
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray()
|
|
|
|
return (1..len).map { chars[random.nextInt(chars.size)] }.joinToString("")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Allows letting multiple variables at once, see https://stackoverflow.com/a/35522422/1440785
|
|
|
|
inline fun <T1: Any, T2: Any, R: Any> safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? {
|
|
|
|
return if (p1 != null && p2 != null) block(p1, p2) else null
|
|
|
|
}
|
2022-01-09 03:32:43 +01:00
|
|
|
|
2022-01-12 01:37:34 +01:00
|
|
|
fun formatBytes(bytes: Long, decimals: Int = 1): String {
|
|
|
|
val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else abs(bytes)
|
2022-01-09 03:32:43 +01:00
|
|
|
if (absB < 1024) {
|
|
|
|
return "$bytes B"
|
|
|
|
}
|
|
|
|
var value = absB
|
|
|
|
val ci = StringCharacterIterator("KMGTPE")
|
|
|
|
var i = 40
|
|
|
|
while (i >= 0 && absB > 0xfffccccccccccccL shr i) {
|
|
|
|
value = value shr 10
|
|
|
|
ci.next()
|
|
|
|
i -= 10
|
|
|
|
}
|
|
|
|
value *= java.lang.Long.signum(bytes).toLong()
|
2022-01-12 01:37:34 +01:00
|
|
|
return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current())
|
2022-01-09 03:32:43 +01:00
|
|
|
}
|
2022-01-09 04:17:41 +01:00
|
|
|
|
|
|
|
fun supportedImage(mimeType: String?): Boolean {
|
|
|
|
return listOf("image/jpeg", "image/png").contains(mimeType)
|
|
|
|
}
|