feat: update to alpha.17, typed mobile plugin IPC arguments (#676)

Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
This commit is contained in:
Lucas Fernandes Nogueira
2023-10-29 16:06:44 -03:00
committed by GitHub
parent 76cfdc32b4
commit e438e0a62d
158 changed files with 1677 additions and 1658 deletions
@@ -12,21 +12,42 @@ import android.graphics.Color
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import app.tauri.Logger
import app.tauri.annotation.InvokeArg
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import com.fasterxml.jackson.annotation.JsonValue
private const val CHANNEL_ID = "id"
private const val CHANNEL_NAME = "name"
private const val CHANNEL_DESCRIPTION = "description"
private const val CHANNEL_IMPORTANCE = "importance"
private const val CHANNEL_VISIBILITY = "visibility"
private const val CHANNEL_SOUND = "sound"
private const val CHANNEL_VIBRATE = "vibration"
private const val CHANNEL_USE_LIGHTS = "lights"
private const val CHANNEL_LIGHT_COLOR = "lightColor"
enum class Importance(@JsonValue val value: Int) {
None(0),
Min(1),
Low(2),
Default(3),
High(4);
}
enum class Visibility(@JsonValue val value: Int) {
Secret(-1),
Private(0),
Public(1);
}
@InvokeArg
class Channel {
lateinit var id: String
lateinit var name: String
var description: String? = null
var sound: String? = null
var lights: Boolean? = null
var lightsColor: String? = null
var vibration: Boolean? = null
var importance: Importance? = null
var visibility: Visibility? = null
}
@InvokeArg
class DeleteChannelArgs {
lateinit var id: String
}
class ChannelManager(private var context: Context) {
private var notificationManager: NotificationManager? = null
@@ -38,32 +59,7 @@ class ChannelManager(private var context: Context) {
fun createChannel(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = JSObject()
if (invoke.getString(CHANNEL_ID) != null) {
channel.put(CHANNEL_ID, invoke.getString(CHANNEL_ID))
} else {
invoke.reject("Channel missing identifier")
return
}
if (invoke.getString(CHANNEL_NAME) != null) {
channel.put(CHANNEL_NAME, invoke.getString(CHANNEL_NAME))
} else {
invoke.reject("Channel missing name")
return
}
channel.put(
CHANNEL_IMPORTANCE,
invoke.getInt(CHANNEL_IMPORTANCE, NotificationManager.IMPORTANCE_DEFAULT)
)
channel.put(CHANNEL_DESCRIPTION, invoke.getString(CHANNEL_DESCRIPTION, ""))
channel.put(
CHANNEL_VISIBILITY,
invoke.getInt(CHANNEL_VISIBILITY, NotificationCompat.VISIBILITY_PUBLIC)
)
channel.put(CHANNEL_SOUND, invoke.getString(CHANNEL_SOUND))
channel.put(CHANNEL_VIBRATE, invoke.getBoolean(CHANNEL_VIBRATE, false))
channel.put(CHANNEL_USE_LIGHTS, invoke.getBoolean(CHANNEL_USE_LIGHTS, false))
channel.put(CHANNEL_LIGHT_COLOR, invoke.getString(CHANNEL_LIGHT_COLOR))
val channel = invoke.parseArgs(Channel::class.java)
createChannel(channel)
invoke.resolve()
} else {
@@ -71,18 +67,18 @@ class ChannelManager(private var context: Context) {
}
}
private fun createChannel(channel: JSObject) {
private fun createChannel(channel: Channel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channel.getString(CHANNEL_ID),
channel.getString(CHANNEL_NAME),
channel.getInteger(CHANNEL_IMPORTANCE)!!
channel.id,
channel.name,
(channel.importance ?: Importance.Default).value
)
notificationChannel.description = channel.getString(CHANNEL_DESCRIPTION)
notificationChannel.lockscreenVisibility = channel.getInteger(CHANNEL_VISIBILITY, android.app.Notification.VISIBILITY_PRIVATE)
notificationChannel.enableVibration(channel.getBoolean(CHANNEL_VIBRATE, false))
notificationChannel.enableLights(channel.getBoolean(CHANNEL_USE_LIGHTS, false))
val lightColor = channel.getString(CHANNEL_LIGHT_COLOR)
notificationChannel.description = channel.description
notificationChannel.lockscreenVisibility = (channel.visibility ?: Visibility.Private).value
notificationChannel.enableVibration(channel.vibration ?: false)
notificationChannel.enableLights(channel.lights ?: false)
val lightColor = channel.lightsColor ?: ""
if (lightColor.isNotEmpty()) {
try {
notificationChannel.lightColor = Color.parseColor(lightColor)
@@ -94,7 +90,7 @@ class ChannelManager(private var context: Context) {
)
}
}
var sound = channel.getString(CHANNEL_SOUND)
var sound = channel.sound ?: ""
if (sound.isNotEmpty()) {
if (sound.contains(".")) {
sound = sound.substring(0, sound.lastIndexOf('.'))
@@ -113,8 +109,8 @@ class ChannelManager(private var context: Context) {
fun deleteChannel(invoke: Invoke) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = invoke.getString("id")
notificationManager?.deleteNotificationChannel(channelId)
val args = invoke.parseArgs(DeleteChannelArgs::class.java)
notificationManager?.deleteNotificationChannel(args.id)
invoke.resolve()
} else {
invoke.reject("channel not available")
@@ -125,28 +121,29 @@ class ChannelManager(private var context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannels: List<NotificationChannel> =
notificationManager?.notificationChannels ?: listOf()
val channels = JSArray()
val channels = mutableListOf<Channel>()
for (notificationChannel in notificationChannels) {
val channel = JSObject()
channel.put(CHANNEL_ID, notificationChannel.id)
channel.put(CHANNEL_NAME, notificationChannel.name)
channel.put(CHANNEL_DESCRIPTION, notificationChannel.description)
channel.put(CHANNEL_IMPORTANCE, notificationChannel.importance)
channel.put(CHANNEL_VISIBILITY, notificationChannel.lockscreenVisibility)
channel.put(CHANNEL_SOUND, notificationChannel.sound)
channel.put(CHANNEL_VIBRATE, notificationChannel.shouldVibrate())
channel.put(CHANNEL_USE_LIGHTS, notificationChannel.shouldShowLights())
channel.put(
CHANNEL_LIGHT_COLOR, String.format(
"#%06X",
0xFFFFFF and notificationChannel.lightColor
)
val channel = Channel()
channel.id = notificationChannel.id
channel.name = notificationChannel.name.toString()
channel.description = notificationChannel.description
channel.sound = notificationChannel.sound.toString()
channel.lights = notificationChannel.shouldShowLights()
String.format(
"#%06X",
0xFFFFFF and notificationChannel.lightColor
)
channels.put(channel)
channel.vibration = notificationChannel.shouldVibrate()
channel.importance = Importance.values().firstOrNull { it.value == notificationChannel.importance }
channel.visibility = Visibility.values().firstOrNull { it.value == notificationChannel.lockscreenVisibility }
channels.add(channel)
}
val result = JSObject()
result.put("channels", channels)
invoke.resolve(result)
invoke.resolveObject(channels)
} else {
invoke.reject("channel not available")
}
@@ -8,20 +8,22 @@ import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import app.tauri.annotation.InvokeArg
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import org.json.JSONException
import org.json.JSONObject
@InvokeArg
class Notification {
var id: Int = 0
var title: String? = null
var body: String? = null
var largeBody: String? = null
var summary: String? = null
var id: Int = 0
private var sound: String? = null
private var smallIcon: String? = null
private var largeIcon: String? = null
var sound: String? = null
var icon: String? = null
var largeIcon: String? = null
var iconColor: String? = null
var actionTypeId: String? = null
var group: String? = null
@@ -33,7 +35,7 @@ class Notification {
var attachments: List<NotificationAttachment>? = null
var schedule: NotificationSchedule? = null
var channelId: String? = null
var source: JSObject? = null
var sourceJson: String? = null
var visibility: Int? = null
var number: Int? = null
@@ -54,18 +56,6 @@ class Notification {
return soundPath
}
fun setSound(sound: String?) {
this.sound = sound
}
fun setSmallIcon(smallIcon: String?) {
this.smallIcon = AssetUtils.getResourceBaseName(smallIcon)
}
fun setLargeIcon(largeIcon: String?) {
this.largeIcon = AssetUtils.getResourceBaseName(largeIcon)
}
fun getIconColor(globalColor: String): String {
// use the one defined local before trying for a globally defined color
return iconColor ?: globalColor
@@ -73,8 +63,8 @@ class Notification {
fun getSmallIcon(context: Context, defaultIcon: Int): Int {
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
if (smallIcon != null) {
resId = AssetUtils.getResourceID(context, smallIcon, "drawable")
if (icon != null) {
resId = AssetUtils.getResourceID(context, icon, "drawable")
}
if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) {
resId = defaultIcon
@@ -93,77 +83,15 @@ class Notification {
val isScheduled = schedule != null
companion object {
fun fromJson(jsonNotification: JSONObject): Notification {
val notification: JSObject = try {
val identifier = jsonNotification.getLong("id")
if (identifier > Int.MAX_VALUE || identifier < Int.MIN_VALUE) {
throw Exception("The notification identifier should be a 32-bit integer")
}
JSObject.fromJSONObject(jsonNotification)
} catch (e: JSONException) {
throw Exception("Invalid notification JSON object", e)
}
return fromJSObject(notification)
}
fun fromJSObject(jsonObject: JSObject): Notification {
val notification = Notification()
notification.source = jsonObject
notification.id = jsonObject.getInteger("id") ?: throw Exception("Missing notification identifier")
notification.body = jsonObject.getString("body", null)
notification.largeBody = jsonObject.getString("largeBody", null)
notification.summary = jsonObject.getString("summary", null)
notification.actionTypeId = jsonObject.getString("actionTypeId", null)
notification.group = jsonObject.getString("group", null)
notification.setSound(jsonObject.getString("sound", null))
notification.title = jsonObject.getString("title", null)
notification.setSmallIcon(jsonObject.getString("icon", null))
notification.setLargeIcon(jsonObject.getString("largeIcon", null))
notification.iconColor = jsonObject.getString("iconColor", null)
notification.attachments = NotificationAttachment.getAttachments(jsonObject)
notification.isGroupSummary = jsonObject.getBoolean("groupSummary", false)
notification.channelId = jsonObject.getString("channelId", null)
val schedule = jsonObject.getJSObject("schedule")
if (schedule != null) {
notification.schedule = NotificationSchedule(schedule)
}
notification.extra = jsonObject.getJSObject("extra")
notification.isOngoing = jsonObject.getBoolean("ongoing", false)
notification.isAutoCancel = jsonObject.getBoolean("autoCancel", true)
notification.visibility = jsonObject.getInteger("visibility")
notification.number = jsonObject.getInteger("number")
try {
val inboxLines = jsonObject.getJSONArray("inboxLines")
val inboxStringList: MutableList<String> = ArrayList()
for (i in 0 until inboxLines.length()) {
inboxStringList.add(inboxLines.getString(i))
}
notification.inboxLines = inboxStringList
} catch (_: Exception) {
}
return notification
}
fun buildNotificationPendingList(notifications: List<Notification>): JSObject {
val result = JSObject()
val jsArray = JSArray()
fun buildNotificationPendingList(notifications: List<Notification>): List<PendingNotification> {
val pendingNotifications = mutableListOf<PendingNotification>()
for (notification in notifications) {
val jsNotification = JSObject()
jsNotification.put("id", notification.id)
jsNotification.put("title", notification.title)
jsNotification.put("body", notification.body)
val schedule = notification.schedule
if (schedule != null) {
val jsSchedule = JSObject()
jsSchedule.put("kind", schedule.scheduleObj.getString("kind", null))
jsSchedule.put("data", schedule.scheduleObj.getJSObject("data"))
jsNotification.put("schedule", jsSchedule)
}
jsNotification.put("extra", notification.extra)
jsArray.put(jsNotification)
val pendingNotification = PendingNotification(notification.id, notification.title, notification.body, notification.schedule, notification.extra)
pendingNotifications.add(pendingNotification)
}
result.put("notifications", jsArray)
return result
return pendingNotifications
}
}
}
}
class PendingNotification(val id: Int, val title: String?, val body: String?, val schedule: NotificationSchedule?, val extra: JSObject?)
@@ -1,51 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
package app.tauri.notification
import app.tauri.Logger
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import org.json.JSONObject
class NotificationAction() {
var id: String? = null
var title: String? = null
var input = false
constructor(id: String?, title: String?, input: Boolean): this() {
this.id = id
this.title = title
this.input = input
}
companion object {
fun buildTypes(types: JSArray): Map<String, List<NotificationAction>> {
val actionTypeMap: MutableMap<String, List<NotificationAction>> = HashMap()
try {
val objects: List<JSONObject> = types.toList()
for (obj in objects) {
val jsObject = JSObject.fromJSONObject(
obj
)
val actionGroupId = jsObject.getString("id")
val actions = jsObject.getJSONArray("actions")
val typesArray = mutableListOf<NotificationAction>()
for (i in 0 until actions.length()) {
val notificationAction = NotificationAction()
val action = JSObject.fromJSONObject(actions.getJSONObject(i))
notificationAction.id = action.getString("id")
notificationAction.title = action.getString("title")
notificationAction.input = action.getBoolean("input")
typesArray.add(notificationAction)
}
actionTypeMap[actionGroupId] = typesArray.toList()
}
} catch (e: Exception) {
Logger.error(Logger.tags("Notification"), "Error when building action types", e)
}
return actionTypeMap
}
}
}
@@ -14,6 +14,7 @@ import android.os.Build
import android.webkit.WebView
import app.tauri.PermissionState
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Permission
import app.tauri.annotation.PermissionCallback
import app.tauri.annotation.TauriPlugin
@@ -21,11 +22,55 @@ import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import org.json.JSONException
import org.json.JSONObject
const val LOCAL_NOTIFICATIONS = "permissionState"
@InvokeArg
class PluginConfig {
var icon: String? = null
var sound: String? = null
var iconColor: String? = null
}
@InvokeArg
class BatchArgs {
lateinit var notifications: List<Notification>
}
@InvokeArg
class CancelArgs {
lateinit var notifications: List<Int>
}
@InvokeArg
class NotificationAction {
lateinit var id: String
var title: String? = null
var input: Boolean? = null
}
@InvokeArg
class ActionType {
lateinit var id: String
lateinit var actions: List<NotificationAction>
}
@InvokeArg
class RegisterActionTypesArgs {
lateinit var types: List<ActionType>
}
@InvokeArg
class ActiveNotification {
var id: Int = 0
var tag: String? = null
}
@InvokeArg
class RemoveActiveArgs {
var notifications: List<ActiveNotification> = listOf()
}
@TauriPlugin(
permissions = [
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "permissionState")
@@ -41,8 +86,8 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
companion object {
var instance: NotificationPlugin? = null
fun triggerNotification(notification: JSObject) {
instance?.trigger("notification", notification)
fun triggerNotification(notification: Notification) {
instance?.triggerObject("notification", notification)
}
}
@@ -51,23 +96,32 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
super.load(webView)
this.webView = webView
notificationStorage = NotificationStorage(activity)
notificationStorage = NotificationStorage(activity, jsonMapper())
val manager = TauriNotificationManager(
notificationStorage,
activity,
activity,
getConfig()
getConfig(PluginConfig::class.java)
)
manager.createNotificationChannel()
this.manager = manager
notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val intent = activity.intent
intent?.let {
onIntent(it)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
onIntent(intent)
}
fun onIntent(intent: Intent) {
if (Intent.ACTION_MAIN != intent.action) {
return
}
@@ -79,80 +133,43 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
@Command
fun show(invoke: Invoke) {
val notification = Notification.fromJSObject(invoke.data)
val notification = invoke.parseArgs(Notification::class.java)
val id = manager.schedule(notification)
val returnVal = JSObject().put("id", id)
invoke.resolve(returnVal)
invoke.resolveObject(id)
}
@Command
fun batch(invoke: Invoke) {
val notificationArray = invoke.getArray("notifications")
if (notificationArray == null) {
invoke.reject("Missing `notifications` argument")
return
}
val args = invoke.parseArgs(BatchArgs::class.java)
val notifications: MutableList<Notification> =
ArrayList(notificationArray.length())
val notificationsInput: List<JSONObject> = try {
notificationArray.toList()
} catch (e: JSONException) {
invoke.reject("Provided notification format is invalid")
return
}
val ids = manager.schedule(args.notifications)
notificationStorage.appendNotifications(args.notifications)
for (jsonNotification in notificationsInput) {
val notification = Notification.fromJson(jsonNotification)
notifications.add(notification)
}
val ids = manager.schedule(notifications)
notificationStorage.appendNotifications(notifications)
val result = JSObject()
result.put("notifications", ids)
invoke.resolve(result)
invoke.resolveObject(ids)
}
@Command
fun cancel(invoke: Invoke) {
val notifications: List<Int> = invoke.getArray("notifications", JSArray()).toList()
if (notifications.isEmpty()) {
invoke.reject("Must provide notifications array as notifications option")
return
}
manager.cancel(notifications)
val args = invoke.parseArgs(CancelArgs::class.java)
manager.cancel(args.notifications)
invoke.resolve()
}
@Command
fun removeActive(invoke: Invoke) {
val notifications = invoke.getArray("notifications")
if (notifications == null) {
val args = invoke.parseArgs(RemoveActiveArgs::class.java)
if (args.notifications.isEmpty()) {
notificationManager.cancelAll()
invoke.resolve()
} else {
try {
for (o in notifications.toList<Any>()) {
if (o is JSONObject) {
val notification = JSObject.fromJSONObject((o))
val tag = notification.getString("tag", null)
val id = notification.getInteger("id", 0)
if (tag == null) {
notificationManager.cancel(id)
} else {
notificationManager.cancel(tag, id)
}
} else {
invoke.reject("Unexpected notification type")
return
}
for (notification in args.notifications) {
if (notification.tag == null) {
notificationManager.cancel(notification.id)
} else {
notificationManager.cancel(notification.tag, notification.id)
}
} catch (e: JSONException) {
invoke.reject(e.message)
}
invoke.resolve()
}
@@ -162,14 +179,13 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
fun getPending(invoke: Invoke) {
val notifications= notificationStorage.getSavedNotifications()
val result = Notification.buildNotificationPendingList(notifications)
invoke.resolve(result)
invoke.resolveObject(result)
}
@Command
fun registerActionTypes(invoke: Invoke) {
val types = invoke.getArray("types", JSArray())
val typesArray = NotificationAction.buildTypes(types)
notificationStorage.writeActionGroup(typesArray)
val args = invoke.parseArgs(RegisterActionTypesArgs::class.java)
notificationStorage.writeActionGroup(args.types)
invoke.resolve()
}
@@ -201,9 +217,8 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
notifications.put(jsNotification)
}
}
val result = JSObject()
result.put("notifications", notifications)
invoke.resolve(result)
invoke.resolveObject(notifications)
}
@Command
@@ -5,13 +5,26 @@
package app.tauri.notification
import android.annotation.SuppressLint
import android.content.ClipData.Item
import android.text.format.DateUtils
import app.tauri.plugin.JSObject
import com.fasterxml.jackson.annotation.JsonFormat
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.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 {
@@ -33,71 +46,89 @@ fun getIntervalTime(interval: NotificationInterval, count: Int): Long {
}
}
sealed class ScheduleKind {
@JsonDeserialize(using = NotificationScheduleDeserializer::class)
@JsonSerialize(using = NotificationScheduleSerializer::class)
sealed class NotificationSchedule {
// At specific moment of time (with repeating option)
class At(var date: Date, val repeating: Boolean): ScheduleKind()
class Interval(val interval: DateMatch): ScheduleKind()
class Every(val interval: NotificationInterval, val count: Int): ScheduleKind()
}
@SuppressLint("SimpleDateFormat")
class NotificationSchedule(val scheduleObj: JSObject) {
val kind: ScheduleKind
// Schedule this notification to fire even if app is idled (Doze)
var whileIdle: Boolean = false
init {
val payload = scheduleObj.getJSObject("data", JSObject())
when (val scheduleKind = scheduleObj.getString("kind", "")) {
"At" -> {
val dateString = payload.getString("date")
if (dateString.isNotEmpty()) {
val sdf = SimpleDateFormat(JS_DATE_FORMAT)
sdf.timeZone = TimeZone.getTimeZone("UTC")
val at = sdf.parse(dateString)
if (at == null) {
throw Exception("could not parse `at` date")
} else {
kind = ScheduleKind.At(at, payload.getBoolean("repeating"))
}
} else {
throw Exception("`at` date cannot be empty")
}
}
"Interval" -> {
val dateMatch = onFromJson(payload)
kind = ScheduleKind.Interval(dateMatch)
}
"Every" -> {
val interval = NotificationInterval.valueOf(payload.getString("interval"))
kind = ScheduleKind.Every(interval, payload.getInteger("count", 1))
}
else -> {
throw Exception("Unknown schedule kind $scheduleKind")
}
}
whileIdle = scheduleObj.getBoolean("allowWhileIdle", false)
}
private fun onFromJson(onJson: JSObject): DateMatch {
val match = DateMatch()
match.year = onJson.getInteger("year")
match.month = onJson.getInteger("month")
match.day = onJson.getInteger("day")
match.weekday = onJson.getInteger("weekday")
match.hour = onJson.getInteger("hour")
match.minute = onJson.getInteger("minute")
match.second = onJson.getInteger("second")
return match
}
class At(@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = JS_DATE_FORMAT) var date: Date, val repeating: Boolean = false, val allowWhileIdle: Boolean = false): NotificationSchedule()
class Interval(val interval: DateMatch, val allowWhileIdle: Boolean = false): NotificationSchedule()
class Every(val interval: NotificationInterval, val count: Int = 0, val allowWhileIdle: Boolean = false): NotificationSchedule()
fun isRemovable(): Boolean {
return when (kind) {
is ScheduleKind.At -> !kind.repeating
return when (this) {
is At -> !repeating
else -> false
}
}
fun allowWhileIdle(): Boolean {
return when (this) {
is At -> allowWhileIdle
is Interval -> allowWhileIdle
is Every -> allowWhileIdle
else -> false
}
}
}
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()
}
else -> {}
}
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 {
@@ -6,23 +6,23 @@ package app.tauri.notification
import android.content.Context
import android.content.SharedPreferences
import app.tauri.plugin.JSObject
import com.fasterxml.jackson.databind.ObjectMapper
import org.json.JSONException
import java.text.ParseException
import java.lang.Exception
// Key for private preferences
private const val NOTIFICATION_STORE_ID = "NOTIFICATION_STORE"
// Key used to save action types
private const val ACTION_TYPES_ID = "ACTION_TYPE_STORE"
class NotificationStorage(private val context: Context) {
class NotificationStorage(private val context: Context, private val jsonMapper: ObjectMapper) {
fun appendNotifications(localNotifications: List<Notification>) {
val storage = getStorage(NOTIFICATION_STORE_ID)
val editor = storage.edit()
for (request in localNotifications) {
if (request.isScheduled) {
val key: String = request.id.toString()
editor.putString(key, request.source.toString())
editor.putString(key, request.sourceJson.toString())
}
}
editor.apply()
@@ -43,57 +43,29 @@ class NotificationStorage(private val context: Context) {
val notifications = ArrayList<Notification>()
for (key in all.keys) {
val notificationString = all[key] as String?
val jsNotification = getNotificationFromJSONString(notificationString)
if (jsNotification != null) {
try {
val notification =
Notification.fromJSObject(jsNotification)
notifications.add(notification)
} catch (_: ParseException) {
}
}
try {
val notification = jsonMapper.readValue(notificationString, Notification::class.java)
notifications.add(notification)
} catch (_: Exception) { }
}
return notifications
}
return ArrayList()
}
private fun getNotificationFromJSONString(notificationString: String?): JSObject? {
if (notificationString == null) {
return null
}
val jsNotification = try {
JSObject(notificationString)
} catch (ex: JSONException) {
return null
}
return jsNotification
}
fun getSavedNotificationAsJSObject(key: String?): JSObject? {
fun getSavedNotification(key: String): Notification? {
val storage = getStorage(NOTIFICATION_STORE_ID)
val notificationString = try {
storage.getString(key, null)
} catch (ex: ClassCastException) {
return null
} ?: return null
val jsNotification = try {
JSObject(notificationString)
} catch (ex: JSONException) {
return null
}
return jsNotification
}
fun getSavedNotification(key: String?): Notification? {
val jsNotification = getSavedNotificationAsJSObject(key) ?: return null
val notification = try {
Notification.fromJSObject(jsNotification)
} catch (ex: ParseException) {
return null
return try {
jsonMapper.readValue(notificationString, Notification::class.java)
} catch (ex: JSONException) {
null
}
return notification
}
fun deleteNotification(id: String?) {
@@ -106,15 +78,16 @@ class NotificationStorage(private val context: Context) {
return context.getSharedPreferences(key, Context.MODE_PRIVATE)
}
fun writeActionGroup(typesMap: Map<String, List<NotificationAction>>) {
for ((id, notificationActions) in typesMap) {
val editor = getStorage(ACTION_TYPES_ID + id).edit()
fun writeActionGroup(actions: List<ActionType>) {
for (type in actions) {
val i = type.id
val editor = getStorage(ACTION_TYPES_ID + type.id).edit()
editor.clear()
editor.putInt("count", notificationActions.size)
for (i in notificationActions.indices) {
editor.putString("id$i", notificationActions[i].id)
editor.putString("title$i", notificationActions[i].title)
editor.putBoolean("input$i", notificationActions[i].input)
editor.putInt("count", type.actions.size)
for (action in type.actions) {
editor.putString("id$i", action.id)
editor.putString("title$i", action.title)
editor.putBoolean("input$i", action.input ?: false)
}
editor.apply()
}
@@ -128,7 +101,12 @@ class NotificationStorage(private val context: Context) {
val id = storage.getString("id$i", "")
val title = storage.getString("title$i", "")
val input = storage.getBoolean("input$i", false)
actions[i] = NotificationAction(id, title, input)
val action = NotificationAction()
action.id = id ?: ""
action.title = title
action.input = input
actions[i] = action
}
return actions
}
@@ -26,6 +26,7 @@ import androidx.core.app.RemoteInput
import app.tauri.Logger
import app.tauri.plugin.JSObject
import app.tauri.plugin.PluginManager
import com.fasterxml.jackson.databind.ObjectMapper
import org.json.JSONException
import org.json.JSONObject
import java.text.SimpleDateFormat
@@ -44,7 +45,7 @@ class TauriNotificationManager(
private val storage: NotificationStorage,
private val activity: Activity?,
private val context: Context,
private val config: JSObject
private val config: PluginConfig?
) {
private var defaultSoundID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
private var defaultSmallIconID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
@@ -200,7 +201,7 @@ class TauriNotificationManager(
mBuilder.setOnlyAlertOnce(true)
mBuilder.setSmallIcon(notification.getSmallIcon(context, getDefaultSmallIcon(context)))
mBuilder.setLargeIcon(notification.getLargeIcon(context))
val iconColor = notification.getIconColor(config.getString("iconColor"))
val iconColor = notification.getIconColor(config?.iconColor ?: "")
if (iconColor.isNotEmpty()) {
try {
mBuilder.color = Color.parseColor(iconColor)
@@ -216,7 +217,7 @@ class TauriNotificationManager(
} else {
notificationManager.notify(notification.id, buildNotification)
try {
NotificationPlugin.triggerNotification(notification.source ?: JSObject())
NotificationPlugin.triggerNotification(notification)
} catch (_: JSONException) {
}
}
@@ -254,7 +255,7 @@ class TauriNotificationManager(
notificationAction.title,
actionPendingIntent
)
if (notificationAction.input) {
if (notificationAction.input == true) {
val remoteInput = RemoteInput.Builder(REMOTE_INPUT_KEY).setLabel(
notificationAction.title
).build()
@@ -298,7 +299,7 @@ class TauriNotificationManager(
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
intent.putExtra(NOTIFICATION_INTENT_KEY, notification.id)
intent.putExtra(ACTION_INTENT_KEY, action)
intent.putExtra(NOTIFICATION_OBJ_INTENT_KEY, notification.source.toString())
intent.putExtra(NOTIFICATION_OBJ_INTENT_KEY, notification.sourceJson)
val schedule = notification.schedule
intent.putExtra(NOTIFICATION_IS_REMOVABLE_KEY, schedule == null || schedule.isRemovable())
return intent
@@ -326,23 +327,22 @@ class TauriNotificationManager(
var pendingIntent =
PendingIntent.getBroadcast(context, request.id, notificationIntent, flags)
when (val scheduleKind = schedule?.kind) {
is ScheduleKind.At -> {
val at = scheduleKind.date
if (at.time < Date().time) {
when (schedule) {
is NotificationSchedule.At -> {
if (schedule.date.time < Date().time) {
Logger.error(Logger.tags("Notification"), "Scheduled time must be *after* current time", null)
return
}
if (scheduleKind.repeating) {
val interval: Long = at.time - Date().time
alarmManager.setRepeating(AlarmManager.RTC, at.time, interval, pendingIntent)
if (schedule.repeating) {
val interval: Long = schedule.date.time - Date().time
alarmManager.setRepeating(AlarmManager.RTC, schedule.date.time, interval, pendingIntent)
} else {
setExactIfPossible(alarmManager, schedule, at.time, pendingIntent)
setExactIfPossible(alarmManager, schedule, schedule.date.time, pendingIntent)
}
}
is ScheduleKind.Interval -> {
val trigger = scheduleKind.interval.nextTrigger(Date())
notificationIntent.putExtra(TimedNotificationPublisher.CRON_KEY, scheduleKind.interval.toMatchString())
is NotificationSchedule.Interval -> {
val trigger = schedule.interval.nextTrigger(Date())
notificationIntent.putExtra(TimedNotificationPublisher.CRON_KEY, schedule.interval.toMatchString())
pendingIntent =
PendingIntent.getBroadcast(context, request.id, notificationIntent, flags)
setExactIfPossible(alarmManager, schedule, trigger, pendingIntent)
@@ -352,8 +352,8 @@ class TauriNotificationManager(
"notification " + request.id + " will next fire at " + sdf.format(Date(trigger))
)
}
is ScheduleKind.Every -> {
val everyInterval = getIntervalTime(scheduleKind.interval, scheduleKind.count)
is NotificationSchedule.Every -> {
val everyInterval = getIntervalTime(schedule.interval, schedule.count)
val startTime: Long = Date().time + everyInterval
alarmManager.setRepeating(AlarmManager.RTC, startTime, everyInterval, pendingIntent)
}
@@ -369,13 +369,13 @@ class TauriNotificationManager(
pendingIntent: PendingIntent
) {
if (SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
if (SDK_INT >= Build.VERSION_CODES.M && schedule.whileIdle) {
if (SDK_INT >= Build.VERSION_CODES.M && schedule.allowWhileIdle()) {
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent)
} else {
alarmManager[AlarmManager.RTC, trigger] = pendingIntent
}
} else {
if (SDK_INT >= Build.VERSION_CODES.M && schedule.whileIdle) {
if (SDK_INT >= Build.VERSION_CODES.M && schedule.allowWhileIdle()) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent)
} else {
alarmManager.setExact(AlarmManager.RTC, trigger, pendingIntent)
@@ -426,7 +426,7 @@ class TauriNotificationManager(
private fun getDefaultSound(context: Context): Int {
if (defaultSoundID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSoundID
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
val soundConfigResourceName = AssetUtils.getResourceBaseName(config.getString("sound"))
val soundConfigResourceName = AssetUtils.getResourceBaseName(config?.sound)
if (soundConfigResourceName != null) {
resId = AssetUtils.getResourceID(context, soundConfigResourceName, "raw")
}
@@ -437,7 +437,7 @@ class TauriNotificationManager(
private fun getDefaultSmallIcon(context: Context): Int {
if (defaultSmallIconID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSmallIconID
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
val smallIconConfigResourceName = AssetUtils.getResourceBaseName(config.getString("icon"))
val smallIconConfigResourceName = AssetUtils.getResourceBaseName(config?.icon)
if (smallIconConfigResourceName != null) {
resId = AssetUtils.getResourceID(context, smallIconConfigResourceName, "drawable")
}
@@ -460,7 +460,7 @@ class NotificationDismissReceiver : BroadcastReceiver() {
val isRemovable =
intent.getBooleanExtra(NOTIFICATION_IS_REMOVABLE_KEY, true)
if (isRemovable) {
val notificationStorage = NotificationStorage(context)
val notificationStorage = NotificationStorage(context, ObjectMapper())
notificationStorage.deleteNotification(intExtra.toString())
}
}
@@ -486,11 +486,13 @@ class TimedNotificationPublisher : BroadcastReceiver() {
if (id == Int.MIN_VALUE) {
Logger.error(Logger.tags("Notification"), "No valid id supplied", null)
}
val storage = NotificationStorage(context)
val notificationJson = storage.getSavedNotificationAsJSObject(id.toString())
if (notificationJson != null) {
NotificationPlugin.triggerNotification(notificationJson)
val storage = NotificationStorage(context, ObjectMapper())
val savedNotification = storage.getSavedNotification(id.toString())
if (savedNotification != null) {
NotificationPlugin.triggerNotification(savedNotification)
}
notificationManager.notify(id, notification)
if (!rescheduleNotificationIfNeeded(context, intent, id)) {
storage.deleteNotification(id.toString())
@@ -545,19 +547,19 @@ class LocalNotificationRestoreReceiver : BroadcastReceiver() {
)
if (um == null || !um.isUserUnlocked) return
}
val storage = NotificationStorage(context)
val storage = NotificationStorage(context, ObjectMapper())
val ids = storage.getSavedNotificationIds()
val notifications = mutableListOf<Notification>()
val updatedNotifications = mutableListOf<Notification>()
for (id in ids) {
val notification = storage.getSavedNotification(id) ?: continue
val schedule = notification.schedule
if (schedule != null && schedule.kind is ScheduleKind.At) {
val at: Date = schedule.kind.date
if (schedule != null && schedule is NotificationSchedule.At) {
val at: Date = schedule.date
if (at.before(Date())) {
// modify the scheduled date in order to show notifications that would have been delivered while device was off.
val newDateTime = Date().time + 15 * 1000
schedule.kind.date = Date(newDateTime)
schedule.date = Date(newDateTime)
updatedNotifications.add(notification)
}
}
@@ -567,7 +569,13 @@ class LocalNotificationRestoreReceiver : BroadcastReceiver() {
storage.appendNotifications(updatedNotifications)
}
val notificationManager = TauriNotificationManager(storage, null, context, PluginManager.loadConfig(context, "notification"))
var config: PluginConfig? = null
try {
config = PluginManager.loadConfig(context, "notification", PluginConfig::class.java)
} catch (ex: Exception) {
ex.printStackTrace()
}
val notificationManager = TauriNotificationManager(storage, null, context, config)
notificationManager.schedule(notifications)
}
}