tauri-plugin-notifications 0.4.5

A Tauri v2 plugin for sending notifications on desktop and mobile platforms with support for system notifications and push delivery via FCM and APNs.
Documentation
package app.tauri.notification

import android.annotation.SuppressLint
import android.text.format.DateUtils
import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.TimeZone

const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"

enum class NotificationInterval {
  @JsonProperty("year")
  Year,
  @JsonProperty("month")
  Month,
  @JsonProperty("twoWeeks")
  TwoWeeks,
  @JsonProperty("week")
  Week,
  @JsonProperty("day")
  Day,
  @JsonProperty("hour")
  Hour,
  @JsonProperty("minute")
  Minute,
  @JsonProperty("second")
  Second
}

fun getIntervalTime(interval: NotificationInterval, count: Int): Long {
  return when (interval) {
    // This case is just approximation as not all years have the same number of days
    NotificationInterval.Year -> count * DateUtils.WEEK_IN_MILLIS * 52
    // This case is just approximation as months have different number of days
    NotificationInterval.Month -> count * 30 * DateUtils.DAY_IN_MILLIS
    NotificationInterval.TwoWeeks -> count * 2 * DateUtils.WEEK_IN_MILLIS
    NotificationInterval.Week -> count * DateUtils.WEEK_IN_MILLIS
    NotificationInterval.Day -> count * DateUtils.DAY_IN_MILLIS
    NotificationInterval.Hour -> count * DateUtils.HOUR_IN_MILLIS
    NotificationInterval.Minute -> count * DateUtils.MINUTE_IN_MILLIS
    NotificationInterval.Second -> count * DateUtils.SECOND_IN_MILLIS
  }
}

@JsonDeserialize(using = NotificationScheduleDeserializer::class)
@JsonSerialize(using = NotificationScheduleSerializer::class)
sealed class NotificationSchedule {
  // At specific moment of time (with repeating option)
  @JsonDeserialize
  class At: NotificationSchedule() {
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = JS_DATE_FORMAT)
    lateinit var date: Date
    var repeating: Boolean = false
    var allowWhileIdle: Boolean = false
  }
  @JsonDeserialize
  class Interval: NotificationSchedule() {
    lateinit var interval: DateMatch
    var allowWhileIdle: Boolean = false
  }
  @JsonDeserialize
  class Every: NotificationSchedule() {
    lateinit var interval: NotificationInterval
    var count: Int = 0
    var allowWhileIdle: Boolean = false
  }

  fun isRemovable(): Boolean {
    return when (this) {
      is At -> !repeating
      else -> false
    }
  }

  fun allowWhileIdle(): Boolean {
    return when (this) {
      is At -> allowWhileIdle
      is Interval -> allowWhileIdle
      is Every -> allowWhileIdle
    }
  }

  override fun toString(): String {
    return when (this) {
      is At -> "At(date=$date, repeating=$repeating, allowWhileIdle=$allowWhileIdle)"
      is Interval -> "Interval(interval=$interval, allowWhileIdle=$allowWhileIdle)"
      is Every -> "Every(interval=$interval, count=$count, allowWhileIdle=$allowWhileIdle)"
    }
  }
}

internal class NotificationScheduleSerializer @JvmOverloads constructor(t: Class<NotificationSchedule>? = null) :
  StdSerializer<NotificationSchedule>(t) {
  @SuppressLint("SimpleDateFormat")
  @Throws(IOException::class, JsonProcessingException::class)
  override fun serialize(
    value: NotificationSchedule, jgen: JsonGenerator, provider: SerializerProvider
  ) {
    jgen.writeStartObject()
    when (value) {
      is NotificationSchedule.At -> {
        jgen.writeObjectFieldStart("at")

        val sdf = SimpleDateFormat(JS_DATE_FORMAT)
        sdf.timeZone = TimeZone.getTimeZone("UTC")
        jgen.writeStringField("date", sdf.format(value.date))
        jgen.writeBooleanField("repeating", value.repeating)

        jgen.writeEndObject()
      }
      is NotificationSchedule.Interval -> {
        jgen.writeObjectFieldStart("interval")

        jgen.writeObjectField("interval", value.interval)

        jgen.writeEndObject()
      }
      is NotificationSchedule.Every -> {
        jgen.writeObjectFieldStart("every")

        jgen.writeObjectField("interval", value.interval)
        jgen.writeNumberField("count", value.count)

        jgen.writeEndObject()
      }
    }

    jgen.writeEndObject()
  }
}

internal class NotificationScheduleDeserializer: JsonDeserializer<NotificationSchedule>() {
  override fun deserialize(
    jsonParser: JsonParser,
    deserializationContext: DeserializationContext
  ): NotificationSchedule {
    val node: JsonNode = jsonParser.codec.readTree(jsonParser)
    node.get("at")?.let {
      return jsonParser.codec.treeToValue(it, NotificationSchedule.At::class.java)
    }
    node.get("interval")?.let {
      return jsonParser.codec.treeToValue(it, NotificationSchedule.Interval::class.java)
    }
    node.get("every")?.let {
      return jsonParser.codec.treeToValue(it, NotificationSchedule.Every::class.java)
    }
    throw Error("unknown schedule kind $node")
  }
}

class DateMatch {
  var year: Int? = null
  var month: Int? = null
  var day: Int? = null
  var weekday: Int? = null
  var hour: Int? = null
  var minute: Int? = null
  var second: Int? = null

  // Unit used to save the last used unit for a trigger.
  // One of the Calendar constants values
  var unit: Int? = -1

  /**
   * Gets a calendar instance pointing to the specified date.
   *
   * @param date The date to point.
   */
  private fun buildCalendar(date: Date): Calendar {
    val cal: Calendar = Calendar.getInstance()
    cal.time = date
    cal.set(Calendar.MILLISECOND, 0)
    return cal
  }

  /**
   * Calculates next trigger date for
   *
   * @param date base date used to calculate trigger
   * @return next trigger timestamp
   */
  fun nextTrigger(date: Date): Long {
    val current: Calendar = buildCalendar(date)
    val next: Calendar = buildNextTriggerTime(date)
    return postponeTriggerIfNeeded(current, next)
  }

  /**
   * Postpone trigger if first schedule matches the past
   */
  private fun postponeTriggerIfNeeded(current: Calendar, next: Calendar): Long {
    if (next.timeInMillis <= current.timeInMillis && unit != -1) {
      var incrementUnit = -1
      if (unit == Calendar.YEAR || unit == Calendar.MONTH) {
        incrementUnit = Calendar.YEAR
      } else if (unit == Calendar.DAY_OF_MONTH) {
        incrementUnit = Calendar.MONTH
      } else if (unit == Calendar.DAY_OF_WEEK) {
        incrementUnit = Calendar.WEEK_OF_MONTH
      } else if (unit == Calendar.HOUR_OF_DAY) {
        incrementUnit = Calendar.DAY_OF_MONTH
      } else if (unit == Calendar.MINUTE) {
        incrementUnit = Calendar.HOUR_OF_DAY
      } else if (unit == Calendar.SECOND) {
        incrementUnit = Calendar.MINUTE
      }
      if (incrementUnit != -1) {
        next.set(incrementUnit, next.get(incrementUnit) + 1)
      }
    }
    return next.timeInMillis
  }

  private fun buildNextTriggerTime(date: Date): Calendar {
    val next: Calendar = buildCalendar(date)
    if (year != null) {
      next.set(Calendar.YEAR, year ?: 0)
      if (unit == -1) unit = Calendar.YEAR
    }
    if (month != null) {
      next.set(Calendar.MONTH, month ?: 0)
      if (unit == -1) unit = Calendar.MONTH
    }
    if (day != null) {
      next.set(Calendar.DAY_OF_MONTH, day ?: 0)
      if (unit == -1) unit = Calendar.DAY_OF_MONTH
    }
    if (weekday != null) {
      next.set(Calendar.DAY_OF_WEEK, weekday ?: 0)
      if (unit == -1) unit = Calendar.DAY_OF_WEEK
    }
    if (hour != null) {
      next.set(Calendar.HOUR_OF_DAY, hour ?: 0)
      if (unit == -1) unit = Calendar.HOUR_OF_DAY
    }
    if (minute != null) {
      next.set(Calendar.MINUTE, minute ?: 0)
      if (unit == -1) unit = Calendar.MINUTE
    }
    if (second != null) {
      next.set(Calendar.SECOND, second ?: 0)
      if (unit == -1) unit = Calendar.SECOND
    }
    return next
  }

  override fun toString(): String {
    return "DateMatch{" +
            "year=" +
            year +
            ", month=" +
            month +
            ", day=" +
            day +
            ", weekday=" +
            weekday +
            ", hour=" +
            hour +
            ", minute=" +
            minute +
            ", second=" +
            second +
            '}'
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other == null || javaClass != other.javaClass) return false
    val dateMatch = other as DateMatch
    if (if (year != null) year != dateMatch.year else dateMatch.year != null) return false
    if (if (month != null) month != dateMatch.month else dateMatch.month != null) return false
    if (if (day != null) day != dateMatch.day else dateMatch.day != null) return false
    if (if (weekday != null) weekday != dateMatch.weekday else dateMatch.weekday != null) return false
    if (if (hour != null) hour != dateMatch.hour else dateMatch.hour != null) return false
    if (if (minute != null) minute != dateMatch.minute else dateMatch.minute != null) return false
    return if (second != null) second == dateMatch.second else dateMatch.second == null
  }

  override fun hashCode(): Int {
    var result = if (year != null) year.hashCode() else 0
    result = 31 * result + if (month != null) month.hashCode() else 0
    result = 31 * result + if (day != null) day.hashCode() else 0
    result = 31 * result + if (weekday != null) weekday.hashCode() else 0
    result = 31 * result + if (hour != null) hour.hashCode() else 0
    result = 31 * result + if (minute != null) minute.hashCode() else 0
    result += 31 + if (second != null) second.hashCode() else 0
    return result
  }

  /**
   * Transform DateMatch object to CronString
   *
   * @return
   */
  fun toMatchString(): String {
    val matchString = year.toString() +
            separator +
            month +
            separator +
            day +
            separator +
            weekday +
            separator +
            hour +
            separator +
            minute +
            separator +
            second +
            separator +
            unit
    return matchString.replace("null", "*")
  }

  companion object {
    private const val separator = " "

    /**
     * Create DateMatch object from stored string
     *
     * @param matchString
     * @return
     */
    fun fromMatchString(matchString: String): DateMatch {
      val date = DateMatch()
      val split = matchString.split(separator.toRegex()).dropLastWhile { it.isEmpty() }
        .toTypedArray()
      if (split.size == 7) {
        date.year = getValueFromCronElement(split[0])
        date.month = getValueFromCronElement(split[1])
        date.day = getValueFromCronElement(split[2])
        date.weekday = getValueFromCronElement(split[3])
        date.hour = getValueFromCronElement(split[4])
        date.minute = getValueFromCronElement(split[5])
        date.unit = getValueFromCronElement(split[6])
      }
      if (split.size == 8) {
        date.year = getValueFromCronElement(split[0])
        date.month = getValueFromCronElement(split[1])
        date.day = getValueFromCronElement(split[2])
        date.weekday = getValueFromCronElement(split[3])
        date.hour = getValueFromCronElement(split[4])
        date.minute = getValueFromCronElement(split[5])
        date.second = getValueFromCronElement(split[6])
        date.unit = getValueFromCronElement(split[7])
      }
      return date
    }

    private fun getValueFromCronElement(token: String): Int? {
      return try {
        token.toInt()
      } catch (e: NumberFormatException) {
        null
      }
    }
  }
}