voltra 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/android/build.gradle +1 -3
  3. package/android/src/main/AndroidManifest.xml +1 -3
  4. package/android/src/main/java/voltra/VoltraModule.kt +120 -23
  5. package/android/src/main/java/voltra/VoltraNotificationManager.kt +674 -100
  6. package/android/src/main/java/voltra/VoltraOngoingNotificationDismissedReceiver.kt +15 -0
  7. package/android/src/main/java/voltra/generated/ShortNames.kt +1 -0
  8. package/android/src/main/java/voltra/glance/GlanceFactory.kt +1 -1
  9. package/android/src/main/java/voltra/glance/RemoteViewsGenerator.kt +1 -1
  10. package/android/src/main/java/voltra/glance/StyleUtils.kt +8 -7
  11. package/android/src/main/java/voltra/glance/VoltraRenderContext.kt +1 -1
  12. package/android/src/main/java/voltra/glance/renderers/ButtonRenderers.kt +16 -18
  13. package/android/src/main/java/voltra/glance/renderers/ChartBitmapRenderer.kt +18 -22
  14. package/android/src/main/java/voltra/glance/renderers/ChartRenderers.kt +31 -17
  15. package/android/src/main/java/voltra/glance/renderers/ComplexRenderers.kt +8 -17
  16. package/android/src/main/java/voltra/glance/renderers/InputRenderers.kt +7 -6
  17. package/android/src/main/java/voltra/glance/renderers/LayoutRenderers.kt +1 -0
  18. package/android/src/main/java/voltra/glance/renderers/LazyListRenderers.kt +2 -2
  19. package/android/src/main/java/voltra/glance/renderers/ProgressRenderers.kt +4 -4
  20. package/android/src/main/java/voltra/glance/renderers/RenderCommon.kt +6 -32
  21. package/android/src/main/java/voltra/glance/renderers/RendererJson.kt +128 -0
  22. package/android/src/main/java/voltra/glance/renderers/TextAndImageRenderers.kt +9 -2
  23. package/android/src/main/java/voltra/glance/renderers/TextBitmapRenderer.kt +4 -1
  24. package/android/src/main/java/voltra/models/VoltraPayload.kt +24 -6
  25. package/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayload.kt +93 -0
  26. package/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayloadParser.kt +13 -0
  27. package/android/src/main/java/voltra/parsing/VoltraDecompressor.kt +10 -10
  28. package/android/src/main/java/voltra/parsing/VoltraPayloadParser.kt +11 -7
  29. package/android/src/main/java/voltra/parsing/VoltraSerializers.kt +231 -0
  30. package/android/src/main/java/voltra/styling/JSColorParser.kt +10 -6
  31. package/android/src/main/java/voltra/styling/JSStyleParser.kt +1 -1
  32. package/android/src/main/java/voltra/styling/StyleConverter.kt +2 -2
  33. package/android/src/main/java/voltra/styling/StyleModifiers.kt +10 -9
  34. package/android/src/main/java/voltra/styling/StyleStructures.kt +4 -6
  35. package/android/src/main/java/voltra/styling/VoltraColorValue.kt +101 -0
  36. package/android/src/main/java/voltra/widget/VoltraRefreshActionCallback.kt +1 -12
  37. package/android/src/main/java/voltra/widget/VoltraWidgetUpdateRequest.kt +30 -0
  38. package/android/src/main/java/voltra/widget/VoltraWidgetUpdateWorker.kt +1 -12
  39. package/build/cjs/VoltraModule.js.map +1 -1
  40. package/build/cjs/android/client.js +1 -1
  41. package/build/cjs/android/client.js.map +1 -1
  42. package/build/cjs/android/components/VoltraView.js +3 -77
  43. package/build/cjs/android/components/VoltraView.js.map +1 -1
  44. package/build/cjs/android/components/VoltraWidgetPreview.js +3 -30
  45. package/build/cjs/android/components/VoltraWidgetPreview.js.map +1 -1
  46. package/build/cjs/android/dynamic-colors.js +6 -0
  47. package/build/cjs/android/dynamic-colors.js.map +1 -0
  48. package/build/cjs/android/index.js +28 -4
  49. package/build/cjs/android/index.js.map +1 -1
  50. package/build/cjs/android/jsx/AreaMark.js.map +1 -1
  51. package/build/cjs/android/jsx/BarMark.js.map +1 -1
  52. package/build/cjs/android/jsx/LineMark.js.map +1 -1
  53. package/build/cjs/android/jsx/PointMark.js.map +1 -1
  54. package/build/cjs/android/jsx/RuleMark.js.map +1 -1
  55. package/build/cjs/android/jsx/SectorMark.js.map +1 -1
  56. package/build/cjs/android/jsx/props/CheckBox.js.map +1 -1
  57. package/build/cjs/android/jsx/props/CircleIconButton.js.map +1 -1
  58. package/build/cjs/android/jsx/props/CircularProgressIndicator.js.map +1 -1
  59. package/build/cjs/android/jsx/props/FilledButton.js.map +1 -1
  60. package/build/cjs/android/jsx/props/Image.js.map +1 -1
  61. package/build/cjs/android/jsx/props/LinearProgressIndicator.js.map +1 -1
  62. package/build/cjs/android/jsx/props/OutlineButton.js.map +1 -1
  63. package/build/cjs/android/jsx/props/RadioButton.js.map +1 -1
  64. package/build/cjs/android/jsx/props/Scaffold.js.map +1 -1
  65. package/build/cjs/android/jsx/props/SquareIconButton.js.map +1 -1
  66. package/build/cjs/android/jsx/props/Switch.js.map +1 -1
  67. package/build/cjs/android/jsx/props/TitleBar.js.map +1 -1
  68. package/build/cjs/android/server.js +9 -15
  69. package/build/cjs/android/server.js.map +1 -1
  70. package/build/cjs/android/styles/types.js.map +1 -1
  71. package/build/cjs/android/widgets/api.js +8 -152
  72. package/build/cjs/android/widgets/api.js.map +1 -1
  73. package/build/cjs/android/widgets/renderer.js +5 -53
  74. package/build/cjs/android/widgets/renderer.js.map +1 -1
  75. package/build/cjs/client.js +24 -24
  76. package/build/cjs/client.js.map +1 -1
  77. package/build/cjs/live-activity/api.js +1 -1
  78. package/build/cjs/live-activity/api.js.map +1 -1
  79. package/build/cjs/styles/index.js.map +1 -1
  80. package/build/cjs/styles/types.js.map +1 -1
  81. package/build/cjs/types.js.map +1 -1
  82. package/build/cjs/widget-server.js +17 -16
  83. package/build/cjs/widget-server.js.map +1 -1
  84. package/build/cjs/widgets/renderer.js +1 -1
  85. package/build/cjs/widgets/renderer.js.map +1 -1
  86. package/build/esm/VoltraModule.js.map +1 -1
  87. package/build/esm/android/client.js +1 -1
  88. package/build/esm/android/client.js.map +1 -1
  89. package/build/esm/android/components/VoltraView.js +1 -43
  90. package/build/esm/android/components/VoltraView.js.map +1 -1
  91. package/build/esm/android/components/VoltraWidgetPreview.js +1 -26
  92. package/build/esm/android/components/VoltraWidgetPreview.js.map +1 -1
  93. package/build/esm/android/dynamic-colors.js +2 -0
  94. package/build/esm/android/dynamic-colors.js.map +1 -0
  95. package/build/esm/android/index.js +3 -1
  96. package/build/esm/android/index.js.map +1 -1
  97. package/build/esm/android/jsx/AreaMark.js.map +1 -1
  98. package/build/esm/android/jsx/BarMark.js.map +1 -1
  99. package/build/esm/android/jsx/LineMark.js.map +1 -1
  100. package/build/esm/android/jsx/PointMark.js.map +1 -1
  101. package/build/esm/android/jsx/RuleMark.js.map +1 -1
  102. package/build/esm/android/jsx/SectorMark.js.map +1 -1
  103. package/build/esm/android/jsx/props/CheckBox.js.map +1 -1
  104. package/build/esm/android/jsx/props/CircleIconButton.js.map +1 -1
  105. package/build/esm/android/jsx/props/CircularProgressIndicator.js.map +1 -1
  106. package/build/esm/android/jsx/props/FilledButton.js.map +1 -1
  107. package/build/esm/android/jsx/props/Image.js.map +1 -1
  108. package/build/esm/android/jsx/props/LinearProgressIndicator.js.map +1 -1
  109. package/build/esm/android/jsx/props/OutlineButton.js.map +1 -1
  110. package/build/esm/android/jsx/props/RadioButton.js.map +1 -1
  111. package/build/esm/android/jsx/props/Scaffold.js.map +1 -1
  112. package/build/esm/android/jsx/props/SquareIconButton.js.map +1 -1
  113. package/build/esm/android/jsx/props/Switch.js.map +1 -1
  114. package/build/esm/android/jsx/props/TitleBar.js.map +1 -1
  115. package/build/esm/android/server.js +2 -1
  116. package/build/esm/android/server.js.map +1 -1
  117. package/build/esm/android/styles/types.js.map +1 -1
  118. package/build/esm/android/widgets/api.js +1 -142
  119. package/build/esm/android/widgets/api.js.map +1 -1
  120. package/build/esm/android/widgets/renderer.js +1 -50
  121. package/build/esm/android/widgets/renderer.js.map +1 -1
  122. package/build/esm/client.js +1 -1
  123. package/build/esm/client.js.map +1 -1
  124. package/build/esm/live-activity/api.js +1 -1
  125. package/build/esm/live-activity/api.js.map +1 -1
  126. package/build/esm/styles/index.js.map +1 -1
  127. package/build/esm/styles/types.js.map +1 -1
  128. package/build/esm/types.js.map +1 -1
  129. package/build/esm/widget-server.js +13 -12
  130. package/build/esm/widget-server.js.map +1 -1
  131. package/build/esm/widgets/renderer.js +1 -1
  132. package/build/esm/widgets/renderer.js.map +1 -1
  133. package/build/types/VoltraModule.d.ts +1 -24
  134. package/build/types/VoltraModule.d.ts.map +1 -1
  135. package/build/types/android/client.d.ts +1 -1
  136. package/build/types/android/components/VoltraView.d.ts +1 -28
  137. package/build/types/android/components/VoltraView.d.ts.map +1 -1
  138. package/build/types/android/components/VoltraWidgetPreview.d.ts +1 -21
  139. package/build/types/android/components/VoltraWidgetPreview.d.ts.map +1 -1
  140. package/build/types/android/dynamic-colors.d.ts +2 -0
  141. package/build/types/android/dynamic-colors.d.ts.map +1 -0
  142. package/build/types/android/index.d.ts +18 -1
  143. package/build/types/android/index.d.ts.map +1 -1
  144. package/build/types/android/jsx/AreaMark.d.ts +2 -1
  145. package/build/types/android/jsx/AreaMark.d.ts.map +1 -1
  146. package/build/types/android/jsx/BarMark.d.ts +2 -1
  147. package/build/types/android/jsx/BarMark.d.ts.map +1 -1
  148. package/build/types/android/jsx/LineMark.d.ts +2 -1
  149. package/build/types/android/jsx/LineMark.d.ts.map +1 -1
  150. package/build/types/android/jsx/PointMark.d.ts +2 -1
  151. package/build/types/android/jsx/PointMark.d.ts.map +1 -1
  152. package/build/types/android/jsx/RuleMark.d.ts +2 -1
  153. package/build/types/android/jsx/RuleMark.d.ts.map +1 -1
  154. package/build/types/android/jsx/SectorMark.d.ts +2 -1
  155. package/build/types/android/jsx/SectorMark.d.ts.map +1 -1
  156. package/build/types/android/jsx/props/CheckBox.d.ts +3 -2
  157. package/build/types/android/jsx/props/CheckBox.d.ts.map +1 -1
  158. package/build/types/android/jsx/props/CircleIconButton.d.ts +3 -2
  159. package/build/types/android/jsx/props/CircleIconButton.d.ts.map +1 -1
  160. package/build/types/android/jsx/props/CircularProgressIndicator.d.ts +2 -1
  161. package/build/types/android/jsx/props/CircularProgressIndicator.d.ts.map +1 -1
  162. package/build/types/android/jsx/props/FilledButton.d.ts +3 -2
  163. package/build/types/android/jsx/props/FilledButton.d.ts.map +1 -1
  164. package/build/types/android/jsx/props/Image.d.ts +3 -2
  165. package/build/types/android/jsx/props/Image.d.ts.map +1 -1
  166. package/build/types/android/jsx/props/LinearProgressIndicator.d.ts +3 -2
  167. package/build/types/android/jsx/props/LinearProgressIndicator.d.ts.map +1 -1
  168. package/build/types/android/jsx/props/OutlineButton.d.ts +2 -1
  169. package/build/types/android/jsx/props/OutlineButton.d.ts.map +1 -1
  170. package/build/types/android/jsx/props/RadioButton.d.ts +3 -2
  171. package/build/types/android/jsx/props/RadioButton.d.ts.map +1 -1
  172. package/build/types/android/jsx/props/Scaffold.d.ts +2 -1
  173. package/build/types/android/jsx/props/Scaffold.d.ts.map +1 -1
  174. package/build/types/android/jsx/props/SquareIconButton.d.ts +3 -2
  175. package/build/types/android/jsx/props/SquareIconButton.d.ts.map +1 -1
  176. package/build/types/android/jsx/props/Switch.d.ts +5 -4
  177. package/build/types/android/jsx/props/Switch.d.ts.map +1 -1
  178. package/build/types/android/jsx/props/TitleBar.d.ts +3 -2
  179. package/build/types/android/jsx/props/TitleBar.d.ts.map +1 -1
  180. package/build/types/android/server.d.ts +4 -1
  181. package/build/types/android/server.d.ts.map +1 -1
  182. package/build/types/android/styles/types.d.ts +5 -4
  183. package/build/types/android/styles/types.d.ts.map +1 -1
  184. package/build/types/android/widgets/api.d.ts +1 -130
  185. package/build/types/android/widgets/api.d.ts.map +1 -1
  186. package/build/types/android/widgets/renderer.d.ts +1 -21
  187. package/build/types/android/widgets/renderer.d.ts.map +1 -1
  188. package/build/types/client.d.ts +2 -2
  189. package/build/types/styles/index.d.ts +1 -1
  190. package/build/types/styles/index.d.ts.map +1 -1
  191. package/build/types/styles/types.d.ts +1 -1
  192. package/build/types/styles/types.d.ts.map +1 -1
  193. package/build/types/types.d.ts +2 -1
  194. package/build/types/types.d.ts.map +1 -1
  195. package/build/types/widget-server.d.ts +4 -4
  196. package/build/types/widget-server.d.ts.map +1 -1
  197. package/ios/shared/ShortNames.swift +1 -0
  198. package/package.json +10 -8
  199. package/android/src/main/java/voltra/parsing/VoltraNodeDeserializer.kt +0 -43
  200. package/build/cjs/android/live-update/api.js +0 -215
  201. package/build/cjs/android/live-update/api.js.map +0 -1
  202. package/build/cjs/android/live-update/renderer.js +0 -46
  203. package/build/cjs/android/live-update/renderer.js.map +0 -1
  204. package/build/cjs/android/live-update/types.js +0 -3
  205. package/build/cjs/android/live-update/types.js.map +0 -1
  206. package/build/esm/android/live-update/api.js +0 -203
  207. package/build/esm/android/live-update/api.js.map +0 -1
  208. package/build/esm/android/live-update/renderer.js +0 -41
  209. package/build/esm/android/live-update/renderer.js.map +0 -1
  210. package/build/esm/android/live-update/types.js +0 -2
  211. package/build/esm/android/live-update/types.js.map +0 -1
  212. package/build/types/android/live-update/api.d.ts +0 -126
  213. package/build/types/android/live-update/api.d.ts.map +0 -1
  214. package/build/types/android/live-update/renderer.d.ts +0 -11
  215. package/build/types/android/live-update/renderer.d.ts.map +0 -1
  216. package/build/types/android/live-update/types.d.ts +0 -100
  217. package/build/types/android/live-update/types.d.ts.map +0 -1
@@ -1,157 +1,731 @@
1
1
  package voltra
2
2
 
3
- import android.app.NotificationChannel
3
+ import android.app.Notification
4
+ import android.app.Notification.BigTextStyle
5
+ import android.app.Notification.Builder
4
6
  import android.app.NotificationManager
7
+ import android.app.PendingIntent
5
8
  import android.content.Context
9
+ import android.content.Intent
10
+ import android.content.pm.PackageManager
11
+ import android.graphics.BitmapFactory
12
+ import android.graphics.drawable.Icon
13
+ import android.net.Uri
6
14
  import android.os.Build
15
+ import android.provider.Settings
7
16
  import android.util.Log
8
- import androidx.core.app.NotificationCompat
17
+ import androidx.compose.ui.graphics.toArgb
9
18
  import kotlinx.coroutines.Dispatchers
10
19
  import kotlinx.coroutines.withContext
11
- import voltra.glance.RemoteViewsGenerator
12
- import voltra.parsing.VoltraPayloadParser
13
- import java.util.concurrent.ConcurrentHashMap
14
- import java.util.concurrent.atomic.AtomicInteger
20
+ import kotlinx.serialization.encodeToString
21
+ import kotlinx.serialization.json.Json
22
+ import voltra.images.VoltraImageManager
23
+ import voltra.ongoingnotification.AndroidOngoingNotificationActionPayload
24
+ import voltra.ongoingnotification.AndroidOngoingNotificationBigTextPayload
25
+ import voltra.ongoingnotification.AndroidOngoingNotificationImageSource
26
+ import voltra.ongoingnotification.AndroidOngoingNotificationPayload
27
+ import voltra.ongoingnotification.AndroidOngoingNotificationPayloadParser
28
+ import voltra.ongoingnotification.AndroidOngoingNotificationProgressPayload
29
+ import voltra.ongoingnotification.AndroidOngoingNotificationProgressPointPayload
30
+ import voltra.ongoingnotification.AndroidOngoingNotificationProgressSegmentPayload
31
+ import voltra.ongoingnotification.AndroidOngoingNotificationRecord
32
+ import voltra.styling.JSColorParser
33
+ import voltra.styling.VoltraColorValue
34
+
35
+ private enum class AndroidOngoingNotificationFallbackBehavior {
36
+ STANDARD,
37
+ ERROR,
38
+ }
39
+
40
+ data class AndroidOngoingNotificationOptions(
41
+ val notificationId: String? = null,
42
+ val channelId: String? = null,
43
+ val smallIcon: String? = null,
44
+ val deepLinkUrl: String? = null,
45
+ val requestPromotedOngoing: Boolean? = null,
46
+ val fallbackBehavior: String? = null,
47
+ )
48
+
49
+ data class AndroidOngoingNotificationCapabilities(
50
+ val apiLevel: Int,
51
+ val notificationsEnabled: Boolean,
52
+ val supportsPromotedNotifications: Boolean,
53
+ val canPostPromotedNotifications: Boolean,
54
+ val canRequestPromotedOngoing: Boolean,
55
+ )
56
+
57
+ data class AndroidOngoingNotificationStatus(
58
+ val isActive: Boolean,
59
+ val isDismissed: Boolean,
60
+ val isPromoted: Boolean? = null,
61
+ val hasPromotableCharacteristics: Boolean? = null,
62
+ )
63
+
64
+ data class AndroidOngoingNotificationStartResult(
65
+ val ok: Boolean,
66
+ val notificationId: String,
67
+ val action: String? = null,
68
+ val reason: String? = null,
69
+ )
70
+
71
+ data class AndroidOngoingNotificationUpdateResult(
72
+ val ok: Boolean,
73
+ val notificationId: String,
74
+ val action: String? = null,
75
+ val reason: String? = null,
76
+ )
77
+
78
+ data class AndroidOngoingNotificationUpsertResult(
79
+ val ok: Boolean,
80
+ val notificationId: String,
81
+ val action: String? = null,
82
+ val reason: String? = null,
83
+ )
84
+
85
+ data class AndroidOngoingNotificationStopResult(
86
+ val ok: Boolean,
87
+ val notificationId: String,
88
+ val action: String? = null,
89
+ val reason: String? = null,
90
+ )
15
91
 
16
92
  class VoltraNotificationManager(
17
- private val context: Context,
93
+ context: Context,
18
94
  ) {
19
95
  companion object {
20
96
  private const val TAG = "VoltraNotificationMgr"
97
+ private const val PREFS_NAME = "voltra_ongoing_notifications"
98
+ private const val KEY_RECORDS = "records"
99
+ private const val KEY_NEXT_NOTIFICATION_ID = "next_notification_id"
100
+ private const val DEFAULT_NOTIFICATION_ID = 10000
101
+ private const val PROMOTED_PERMISSION = "android.permission.POST_PROMOTED_NOTIFICATIONS"
102
+ private const val EXTRA_REQUEST_PROMOTED_ONGOING = "android.requestPromotedOngoing"
103
+ const val EXTRA_NOTIFICATION_ID = "voltra.extra.NOTIFICATION_ID"
104
+
105
+ private val json =
106
+ Json {
107
+ ignoreUnknownKeys = true
108
+ encodeDefaults = true
109
+ }
110
+
111
+ fun markDismissed(
112
+ context: Context,
113
+ notificationId: String,
114
+ ) {
115
+ val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
116
+ val records = readRecords(prefs).toMutableMap()
117
+ val record = records[notificationId] ?: return
118
+
119
+ records[notificationId] =
120
+ record.copy(
121
+ active = false,
122
+ dismissed = true,
123
+ )
124
+ writeRecords(prefs, records)
125
+ Log.d(TAG, "Marked ongoing notification as dismissed: $notificationId")
126
+ }
127
+
128
+ private fun readRecords(
129
+ prefs: android.content.SharedPreferences,
130
+ ): Map<String, AndroidOngoingNotificationRecord> {
131
+ val raw = prefs.getString(KEY_RECORDS, null) ?: return emptyMap()
132
+ return try {
133
+ json.decodeFromString<Map<String, AndroidOngoingNotificationRecord>>(raw)
134
+ } catch (error: Exception) {
135
+ Log.e(TAG, "Failed to decode ongoing notification records", error)
136
+ emptyMap()
137
+ }
138
+ }
139
+
140
+ private fun writeRecords(
141
+ prefs: android.content.SharedPreferences,
142
+ records: Map<String, AndroidOngoingNotificationRecord>,
143
+ ) {
144
+ prefs.edit().putString(KEY_RECORDS, json.encodeToString(records)).commit()
145
+ }
21
146
  }
22
147
 
148
+ private val appContext = context.applicationContext
23
149
  private val notificationManager =
24
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
25
- private val activeNotifications = ConcurrentHashMap<String, Int>()
26
- private val idCounter = AtomicInteger(10000)
150
+ appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
151
+ private val prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
152
+ private val lock = Any()
27
153
 
28
- suspend fun startLiveUpdate(
154
+ suspend fun startOngoingNotification(
29
155
  payload: String,
30
- updateName: String?,
31
- channelId: String,
32
- ): String =
156
+ options: AndroidOngoingNotificationOptions,
157
+ ): AndroidOngoingNotificationStartResult =
33
158
  withContext(Dispatchers.Default) {
34
- Log.d(TAG, "startLiveUpdate called with updateName=$updateName, channelId=$channelId")
35
- Log.d(TAG, "Payload (first 200 chars): ${payload.take(200)}")
159
+ val notificationId = options.notificationId ?: createGeneratedNotificationId()
160
+ val existingRecord = getRecord(notificationId)
161
+ if (existingRecord != null) {
162
+ return@withContext AndroidOngoingNotificationStartResult(
163
+ ok = false,
164
+ notificationId = notificationId,
165
+ reason = "already_exists",
166
+ )
167
+ }
168
+ val record =
169
+ createMergedRecord(
170
+ notificationId = notificationId,
171
+ currentRecord = existingRecord,
172
+ options = options,
173
+ allowMissingChannel = false,
174
+ ).copy(
175
+ active = true,
176
+ dismissed = false,
177
+ )
36
178
 
37
- val voltraPayload = VoltraPayloadParser.parse(payload)
38
- val notificationId = updateName ?: "live-update-${idCounter.getAndIncrement()}"
39
- val intId = notificationId.hashCode().and(0x7FFFFFFF) // Ensure positive
179
+ postNotification(
180
+ record,
181
+ AndroidOngoingNotificationPayloadParser.parse(payload),
182
+ onlyAlertOnce = existingRecord != null,
183
+ )
184
+ saveRecord(record)
185
+ AndroidOngoingNotificationStartResult(
186
+ ok = true,
187
+ notificationId = notificationId,
188
+ action = "started",
189
+ )
190
+ }
40
191
 
41
- Log.d(TAG, "Parsed payload, notificationId=$notificationId, intId=$intId")
192
+ suspend fun updateOngoingNotification(
193
+ notificationId: String,
194
+ payload: String,
195
+ options: AndroidOngoingNotificationOptions?,
196
+ ): AndroidOngoingNotificationUpdateResult =
197
+ withContext(Dispatchers.Default) {
198
+ val currentRecord =
199
+ getRecord(notificationId)
200
+ ?: return@withContext AndroidOngoingNotificationUpdateResult(
201
+ ok = false,
202
+ notificationId = notificationId,
203
+ reason = "not_found",
204
+ )
205
+ if (currentRecord.dismissed) {
206
+ Log.d(TAG, "Rejected dismissed ongoing notification $notificationId")
207
+ return@withContext AndroidOngoingNotificationUpdateResult(
208
+ ok = false,
209
+ notificationId = notificationId,
210
+ reason = "dismissed",
211
+ )
212
+ }
42
213
 
43
- createNotificationChannel(channelId)
214
+ val record =
215
+ createMergedRecord(
216
+ notificationId = notificationId,
217
+ currentRecord = currentRecord,
218
+ options = options ?: AndroidOngoingNotificationOptions(),
219
+ allowMissingChannel = currentRecord != null,
220
+ ).copy(
221
+ active = true,
222
+ dismissed = false,
223
+ )
44
224
 
45
- val collapsedView = RemoteViewsGenerator.generateCollapsed(context, voltraPayload)
46
- val expandedView = RemoteViewsGenerator.generateExpanded(context, voltraPayload)
225
+ postNotification(record, AndroidOngoingNotificationPayloadParser.parse(payload), onlyAlertOnce = true)
226
+ saveRecord(record)
227
+ AndroidOngoingNotificationUpdateResult(
228
+ ok = true,
229
+ notificationId = notificationId,
230
+ action = "updated",
231
+ )
232
+ }
47
233
 
48
- Log.d(TAG, "Generated views: collapsed=${collapsedView != null}, expanded=${expandedView != null}")
234
+ suspend fun upsertOngoingNotification(
235
+ payload: String,
236
+ options: AndroidOngoingNotificationOptions,
237
+ ): AndroidOngoingNotificationUpsertResult =
238
+ withContext(Dispatchers.Default) {
239
+ val notificationId = options.notificationId ?: createGeneratedNotificationId()
240
+ val currentRecord = getRecord(notificationId)
49
241
 
50
- val notification =
51
- NotificationCompat
52
- .Builder(context, channelId)
53
- .setSmallIcon(getSmallIcon(voltraPayload.smallIcon))
54
- .setOngoing(true)
55
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
56
- .apply {
57
- collapsedView?.let { setCustomContentView(it) }
58
- expandedView?.let { setCustomBigContentView(it) }
59
- }.build()
242
+ if (currentRecord == null) {
243
+ val startResult = startOngoingNotification(payload, options.copy(notificationId = notificationId))
244
+ return@withContext AndroidOngoingNotificationUpsertResult(
245
+ ok = startResult.ok,
246
+ notificationId = startResult.notificationId,
247
+ action = if (startResult.ok) "started" else null,
248
+ reason = startResult.reason,
249
+ )
250
+ }
60
251
 
61
- notificationManager.notify(intId, notification)
62
- activeNotifications[notificationId] = intId
252
+ val updateResult = updateOngoingNotification(notificationId, payload, options.copy(notificationId = null))
253
+ AndroidOngoingNotificationUpsertResult(
254
+ ok = updateResult.ok,
255
+ notificationId = notificationId,
256
+ action = if (updateResult.ok) "updated" else null,
257
+ reason = updateResult.reason,
258
+ )
259
+ }
63
260
 
64
- Log.d(TAG, "Notification posted. Active notifications: ${activeNotifications.keys}")
261
+ fun stopOngoingNotification(notificationId: String): AndroidOngoingNotificationStopResult {
262
+ val record =
263
+ getRecord(notificationId)
264
+ ?: return AndroidOngoingNotificationStopResult(
265
+ ok = false,
266
+ notificationId = notificationId,
267
+ reason = "not_found",
268
+ )
269
+ notificationManager.cancel(record.systemNotificationId)
270
+ removeRecord(notificationId)
271
+ return AndroidOngoingNotificationStopResult(
272
+ ok = true,
273
+ notificationId = notificationId,
274
+ action = "stopped",
275
+ )
276
+ }
65
277
 
66
- notificationId
278
+ fun isOngoingNotificationActive(notificationId: String): Boolean =
279
+ getOngoingNotificationStatus(notificationId).isActive
280
+
281
+ fun getOngoingNotificationStatus(notificationId: String): AndroidOngoingNotificationStatus {
282
+ val record = getRecord(notificationId)
283
+ if (record == null) {
284
+ return AndroidOngoingNotificationStatus(
285
+ isActive = false,
286
+ isDismissed = false,
287
+ )
67
288
  }
68
289
 
69
- suspend fun updateLiveUpdate(
70
- notificationId: String,
71
- payload: String,
72
- ) = withContext(Dispatchers.Default) {
73
- Log.d(TAG, "updateLiveUpdate called with notificationId=$notificationId")
74
- Log.d(TAG, "Active notifications: ${activeNotifications.keys}")
290
+ val activeNotification = getActiveStatusBarNotification(record.systemNotificationId)
291
+ val notification = activeNotification?.notification
292
+ val isActive = activeNotification != null
293
+ val isPromoted =
294
+ if (Build.VERSION.SDK_INT >= 36 && notification != null) {
295
+ (notification.flags and Notification.FLAG_PROMOTED_ONGOING) != 0
296
+ } else {
297
+ null
298
+ }
299
+ val hasPromotableCharacteristics =
300
+ if (Build.VERSION.SDK_INT >= 36 && notification != null) {
301
+ notification.hasPromotableCharacteristics()
302
+ } else {
303
+ null
304
+ }
305
+
306
+ return AndroidOngoingNotificationStatus(
307
+ isActive = isActive,
308
+ isDismissed = record.dismissed,
309
+ isPromoted = isPromoted,
310
+ hasPromotableCharacteristics = hasPromotableCharacteristics,
311
+ )
312
+ }
75
313
 
76
- val intId = activeNotifications[notificationId]
77
- if (intId == null) {
78
- Log.e(TAG, "Notification $notificationId not found in activeNotifications!")
79
- return@withContext
314
+ fun endAllOngoingNotifications() {
315
+ val records = getRecords()
316
+ records.values.forEach { record ->
317
+ notificationManager.cancel(record.systemNotificationId)
80
318
  }
319
+ clearRecords()
320
+ }
321
+
322
+ fun canPostPromotedAndroidNotifications(): Boolean =
323
+ getOngoingNotificationCapabilities().canPostPromotedNotifications
324
+
325
+ fun getOngoingNotificationCapabilities(): AndroidOngoingNotificationCapabilities {
326
+ val notificationsEnabled = notificationManager.areNotificationsEnabled()
327
+ val supportsPromoted = Build.VERSION.SDK_INT >= 36
328
+ val canPostPromoted =
329
+ supportsPromoted &&
330
+ notificationsEnabled &&
331
+ notificationManager.canPostPromotedNotifications() &&
332
+ hasPromotedNotificationsPermission()
81
333
 
82
- Log.d(TAG, "Found intId=$intId for notificationId=$notificationId")
334
+ return AndroidOngoingNotificationCapabilities(
335
+ apiLevel = Build.VERSION.SDK_INT,
336
+ notificationsEnabled = notificationsEnabled,
337
+ supportsPromotedNotifications = supportsPromoted,
338
+ canPostPromotedNotifications = canPostPromoted,
339
+ canRequestPromotedOngoing = canPostPromoted,
340
+ )
341
+ }
83
342
 
84
- val voltraPayload = VoltraPayloadParser.parse(payload)
85
- val channelId = voltraPayload.channelId ?: "voltra_live_updates"
343
+ fun openPromotedNotificationSettings() {
344
+ val intent =
345
+ Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
346
+ putExtra(Settings.EXTRA_APP_PACKAGE, appContext.packageName)
347
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
348
+ }
349
+ appContext.startActivity(intent)
350
+ }
86
351
 
87
- val collapsedView = RemoteViewsGenerator.generateCollapsed(context, voltraPayload)
88
- val expandedView = RemoteViewsGenerator.generateExpanded(context, voltraPayload)
352
+ private fun createGeneratedNotificationId(): String {
353
+ val intId = allocateNotificationId()
354
+ return "ongoing-notification-$intId"
355
+ }
89
356
 
90
- Log.d(TAG, "Update generated views: collapsed=${collapsedView != null}, expanded=${expandedView != null}")
357
+ private fun postNotification(
358
+ record: AndroidOngoingNotificationRecord,
359
+ payload: AndroidOngoingNotificationPayload,
360
+ onlyAlertOnce: Boolean,
361
+ ) {
362
+ if (record.requestPromotedOngoing &&
363
+ resolveFallbackBehavior(record.fallbackBehavior) == AndroidOngoingNotificationFallbackBehavior.ERROR
364
+ ) {
365
+ val capabilities = getOngoingNotificationCapabilities()
366
+ if (!capabilities.canRequestPromotedOngoing) {
367
+ throw IllegalStateException(
368
+ "Promoted ongoing notifications are unavailable on this device/app configuration.",
369
+ )
370
+ }
371
+ }
91
372
 
92
- val notification =
93
- NotificationCompat
94
- .Builder(context, channelId)
95
- .setSmallIcon(getSmallIcon(voltraPayload.smallIcon))
373
+ val builder =
374
+ Builder(appContext, record.channelId)
375
+ .setSmallIcon(resolveSmallIcon(record.smallIcon))
96
376
  .setOngoing(true)
97
- .setOnlyAlertOnce(true) // Don't make sound/vibration on updates
98
- .setWhen(System.currentTimeMillis()) // Force timestamp update
99
- .setShowWhen(false) // But don't show the time
100
- .apply {
101
- collapsedView?.let { setCustomContentView(it) }
102
- expandedView?.let { setCustomBigContentView(it) }
103
- }.build()
377
+ .setOnlyAlertOnce(onlyAlertOnce)
378
+ .setDeleteIntent(createDeleteIntent(record))
379
+ .setContentIntent(createContentIntent(record))
380
+
381
+ getNotificationCategory(payload)?.let { builder.setCategory(it) }
104
382
 
105
- // Force notification flags to allow updates
106
- notification.flags = notification.flags or android.app.Notification.FLAG_ONGOING_EVENT
383
+ payload.title?.let { builder.setContentTitle(it) }
107
384
 
108
- notificationManager.notify(intId, notification)
109
- Log.d(TAG, "Notification updated successfully")
385
+ applyCommonFields(builder, payload)
386
+ applyPayloadStyle(builder, payload)
387
+ applyActions(builder, record, payload)
388
+ requestPromotionIfPossible(builder, record)
389
+
390
+ notificationManager.notify(record.systemNotificationId, builder.build())
110
391
  }
111
392
 
112
- fun stopLiveUpdate(notificationId: String) {
113
- Log.d(TAG, "stopLiveUpdate called with notificationId=$notificationId")
114
- activeNotifications.remove(notificationId)?.let { intId ->
115
- notificationManager.cancel(intId)
116
- Log.d(TAG, "Notification cancelled")
393
+ private fun applyCommonFields(
394
+ builder: Builder,
395
+ payload: AndroidOngoingNotificationPayload,
396
+ ) {
397
+ when (payload) {
398
+ is AndroidOngoingNotificationProgressPayload -> {
399
+ builder.setContentText(payload.text)
400
+ builder.setProgress(payload.max, payload.value, payload.indeterminate == true)
401
+ }
402
+
403
+ is AndroidOngoingNotificationBigTextPayload -> {
404
+ builder.setContentText(payload.text)
405
+ }
406
+ }
407
+
408
+ payload.subText?.let { builder.setSubText(it) }
409
+
410
+ resolveNotificationIcon(payload.largeIcon)?.let { builder.setLargeIcon(it) }
411
+
412
+ if (Build.VERSION.SDK_INT >= 36) {
413
+ payload.shortCriticalText?.let { builder.setShortCriticalText(it) }
414
+ }
415
+
416
+ if (payload.whenEpochMillis != null || payload.chronometer == true) {
417
+ builder.setWhen(payload.whenEpochMillis ?: System.currentTimeMillis())
418
+ builder.setShowWhen(true)
419
+ builder.setUsesChronometer(payload.chronometer == true)
420
+ } else {
421
+ builder.setShowWhen(false)
117
422
  }
118
423
  }
119
424
 
120
- fun isLiveUpdateActive(updateName: String): Boolean = activeNotifications.containsKey(updateName)
425
+ private fun applyPayloadStyle(
426
+ builder: Builder,
427
+ payload: AndroidOngoingNotificationPayload,
428
+ ) {
429
+ when (payload) {
430
+ is AndroidOngoingNotificationBigTextPayload -> {
431
+ builder.setStyle(BigTextStyle().bigText(payload.bigText ?: payload.text))
432
+ }
433
+
434
+ is AndroidOngoingNotificationProgressPayload -> {
435
+ if (Build.VERSION.SDK_INT >= 36) {
436
+ val style =
437
+ Notification
438
+ .ProgressStyle()
439
+ .setProgress(
440
+ payload.value,
441
+ ).setProgressIndeterminate(payload.indeterminate == true)
442
+ .setStyledByProgress(true)
443
+
444
+ resolveNotificationIcon(payload.progressTrackerIcon)?.let { style.setProgressTrackerIcon(it) }
445
+ resolveNotificationIcon(payload.progressStartIcon)?.let { style.setProgressStartIcon(it) }
446
+ resolveNotificationIcon(payload.progressEndIcon)?.let { style.setProgressEndIcon(it) }
121
447
 
122
- fun endAllLiveUpdates() {
123
- Log.d(TAG, "endAllLiveUpdates called")
124
- activeNotifications.forEach { (_, intId) ->
125
- notificationManager.cancel(intId)
448
+ payload.segments?.forEach { segment ->
449
+ style.addProgressSegment(segment.toNativeSegment())
450
+ }
451
+
452
+ payload.points?.forEach { point ->
453
+ style.addProgressPoint(point.toNativePoint())
454
+ }
455
+
456
+ builder.setStyle(style)
457
+ }
458
+ }
126
459
  }
127
- activeNotifications.clear()
128
460
  }
129
461
 
130
- private fun createNotificationChannel(channelId: String) {
131
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
132
- val channel =
133
- NotificationChannel(
134
- channelId,
135
- "Voltra Live Updates",
136
- NotificationManager.IMPORTANCE_DEFAULT,
137
- ).apply {
138
- description = "Live update notifications from Voltra"
462
+ private fun getNotificationCategory(payload: AndroidOngoingNotificationPayload): String? =
463
+ when (payload) {
464
+ is AndroidOngoingNotificationProgressPayload -> Notification.CATEGORY_PROGRESS
465
+ is AndroidOngoingNotificationBigTextPayload -> null
466
+ }
467
+
468
+ private fun resolveNotificationIcon(source: AndroidOngoingNotificationImageSource?): Icon? {
469
+ if (source == null) return null
470
+
471
+ source.assetName?.takeIf { it.isNotBlank() }?.let { assetName ->
472
+ val resId = appContext.resources.getIdentifier(assetName, "drawable", appContext.packageName)
473
+ if (resId != 0) {
474
+ return Icon.createWithResource(appContext, resId)
475
+ }
476
+
477
+ val imageManager = VoltraImageManager(appContext)
478
+ val uriString = imageManager.getUriForKey(assetName)
479
+ if (uriString != null) {
480
+ try {
481
+ val uri = Uri.parse(uriString)
482
+ appContext.contentResolver.openInputStream(uri)?.use { stream ->
483
+ val bitmap = BitmapFactory.decodeStream(stream)
484
+ if (bitmap != null) {
485
+ return Icon.createWithBitmap(bitmap)
486
+ }
487
+ }
488
+ } catch (error: Exception) {
489
+ Log.e(TAG, "Failed to decode notification icon asset: $assetName", error)
139
490
  }
140
- notificationManager.createNotificationChannel(channel)
141
- Log.d(TAG, "Notification channel created: $channelId")
491
+ }
142
492
  }
493
+
494
+ source.base64?.takeIf { it.isNotBlank() }?.let { base64 ->
495
+ try {
496
+ val decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT)
497
+ val bitmap = BitmapFactory.decodeByteArray(decoded, 0, decoded.size)
498
+ if (bitmap != null) {
499
+ return Icon.createWithBitmap(bitmap)
500
+ }
501
+ } catch (error: Exception) {
502
+ Log.e(TAG, "Failed to decode notification base64 icon", error)
503
+ }
504
+ }
505
+
506
+ return null
507
+ }
508
+
509
+ private fun getActiveStatusBarNotification(
510
+ systemNotificationId: Int,
511
+ ): android.service.notification.StatusBarNotification? =
512
+ if (Build.VERSION.SDK_INT >= 23) {
513
+ notificationManager.activeNotifications.firstOrNull { notification ->
514
+ notification.id == systemNotificationId && notification.packageName == appContext.packageName
515
+ }
516
+ } else {
517
+ null
518
+ }
519
+
520
+ private fun AndroidOngoingNotificationProgressSegmentPayload.toNativeSegment(): Notification.ProgressStyle.Segment {
521
+ val segment = Notification.ProgressStyle.Segment(length)
522
+ parseAndroidColor(color)?.let { segment.setColor(it) }
523
+ return segment
524
+ }
525
+
526
+ private fun AndroidOngoingNotificationProgressPointPayload.toNativePoint(): Notification.ProgressStyle.Point {
527
+ val point = Notification.ProgressStyle.Point(position)
528
+ parseAndroidColor(color)?.let { point.setColor(it) }
529
+ return point
530
+ }
531
+
532
+ private fun parseAndroidColor(color: String?): Int? {
533
+ val value = JSColorParser.parse(color) as? VoltraColorValue.Static ?: return null
534
+ return value.color.toArgb()
535
+ }
536
+
537
+ private fun requestPromotionIfPossible(
538
+ builder: Builder,
539
+ record: AndroidOngoingNotificationRecord,
540
+ ) {
541
+ if (!record.requestPromotedOngoing || !getOngoingNotificationCapabilities().canRequestPromotedOngoing) {
542
+ return
543
+ }
544
+
545
+ val extras = builder.extras ?: android.os.Bundle()
546
+ extras.putBoolean(EXTRA_REQUEST_PROMOTED_ONGOING, true)
547
+ builder.setExtras(extras)
143
548
  }
144
549
 
145
- private fun getSmallIcon(iconName: String?): Int {
146
- if (iconName != null) {
147
- val resId =
148
- context.resources.getIdentifier(
149
- iconName,
150
- "drawable",
151
- context.packageName,
550
+ private fun applyActions(
551
+ builder: Builder,
552
+ record: AndroidOngoingNotificationRecord,
553
+ payload: AndroidOngoingNotificationPayload,
554
+ ) {
555
+ payload.actions?.forEachIndexed { index, action ->
556
+ val pendingIntent = createActionIntent(record, action, index) ?: return@forEachIndexed
557
+ val actionBuilder =
558
+ Notification.Action.Builder(
559
+ resolveNotificationIcon(action.icon),
560
+ action.title,
561
+ pendingIntent,
152
562
  )
153
- if (resId != 0) return resId
563
+ builder.addAction(actionBuilder.build())
564
+ }
565
+ }
566
+
567
+ private fun createContentIntent(record: AndroidOngoingNotificationRecord): PendingIntent? {
568
+ val intent =
569
+ createLaunchIntent(record.deepLinkUrl)
570
+ ?: appContext.packageManager.getLaunchIntentForPackage(appContext.packageName)?.apply {
571
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
572
+ }
573
+
574
+ return intent?.let {
575
+ PendingIntent.getActivity(
576
+ appContext,
577
+ record.systemNotificationId,
578
+ it,
579
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
580
+ )
581
+ }
582
+ }
583
+
584
+ private fun createActionIntent(
585
+ record: AndroidOngoingNotificationRecord,
586
+ action: AndroidOngoingNotificationActionPayload,
587
+ index: Int,
588
+ ): PendingIntent? {
589
+ val intent = createLaunchIntent(action.deepLinkUrl) ?: return null
590
+
591
+ return PendingIntent.getActivity(
592
+ appContext,
593
+ createActionRequestCode(record.systemNotificationId, index),
594
+ intent,
595
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
596
+ )
597
+ }
598
+
599
+ private fun createLaunchIntent(deepLinkUrl: String?): Intent? {
600
+ if (deepLinkUrl.isNullOrBlank()) {
601
+ return null
602
+ }
603
+
604
+ return Intent(Intent.ACTION_VIEW, Uri.parse(deepLinkUrl)).apply {
605
+ setPackage(appContext.packageName)
606
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
607
+ }
608
+ }
609
+
610
+ private fun createActionRequestCode(
611
+ notificationId: Int,
612
+ index: Int,
613
+ ): Int = (notificationId * 100) + index + 1
614
+
615
+ private fun createDeleteIntent(record: AndroidOngoingNotificationRecord): PendingIntent {
616
+ val intent =
617
+ Intent(appContext, VoltraOngoingNotificationDismissedReceiver::class.java).apply {
618
+ putExtra(EXTRA_NOTIFICATION_ID, record.notificationId)
619
+ }
620
+
621
+ return PendingIntent.getBroadcast(
622
+ appContext,
623
+ record.systemNotificationId,
624
+ intent,
625
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
626
+ )
627
+ }
628
+
629
+ private fun resolveSmallIcon(iconName: String?): Int {
630
+ if (!iconName.isNullOrBlank()) {
631
+ val drawableId = appContext.resources.getIdentifier(iconName, "drawable", appContext.packageName)
632
+ if (drawableId != 0) {
633
+ return drawableId
634
+ }
635
+
636
+ val mipmapId = appContext.resources.getIdentifier(iconName, "mipmap", appContext.packageName)
637
+ if (mipmapId != 0) {
638
+ return mipmapId
639
+ }
640
+ }
641
+
642
+ return appContext.applicationInfo.icon
643
+ }
644
+
645
+ private fun resolveFallbackBehavior(fallbackBehavior: String?): AndroidOngoingNotificationFallbackBehavior =
646
+ if (fallbackBehavior.equals("error", ignoreCase = true)) {
647
+ AndroidOngoingNotificationFallbackBehavior.ERROR
648
+ } else {
649
+ AndroidOngoingNotificationFallbackBehavior.STANDARD
650
+ }
651
+
652
+ private fun hasPromotedNotificationsPermission(): Boolean {
653
+ if (Build.VERSION.SDK_INT < 36) {
654
+ return false
655
+ }
656
+
657
+ return try {
658
+ appContext.checkSelfPermission(PROMOTED_PERMISSION) == PackageManager.PERMISSION_GRANTED
659
+ } catch (_: Throwable) {
660
+ true
661
+ }
662
+ }
663
+
664
+ private fun createMergedRecord(
665
+ notificationId: String,
666
+ currentRecord: AndroidOngoingNotificationRecord?,
667
+ options: AndroidOngoingNotificationOptions,
668
+ allowMissingChannel: Boolean,
669
+ ): AndroidOngoingNotificationRecord {
670
+ val channelId = options.channelId ?: currentRecord?.channelId
671
+ if (channelId.isNullOrBlank() && !allowMissingChannel) {
672
+ throw IllegalArgumentException("channelId is required for Android ongoing notifications.")
673
+ }
674
+
675
+ val systemNotificationId = currentRecord?.systemNotificationId ?: allocateNotificationId()
676
+
677
+ return AndroidOngoingNotificationRecord(
678
+ notificationId = notificationId,
679
+ systemNotificationId = systemNotificationId,
680
+ channelId =
681
+ channelId ?: throw IllegalArgumentException("channelId is required for Android ongoing notifications."),
682
+ smallIcon = options.smallIcon ?: currentRecord?.smallIcon,
683
+ deepLinkUrl = options.deepLinkUrl ?: currentRecord?.deepLinkUrl,
684
+ requestPromotedOngoing =
685
+ if (options.requestPromotedOngoing != null) {
686
+ options.requestPromotedOngoing
687
+ } else {
688
+ currentRecord?.requestPromotedOngoing ?: false
689
+ },
690
+ fallbackBehavior = options.fallbackBehavior ?: currentRecord?.fallbackBehavior ?: "standard",
691
+ active = currentRecord?.active ?: true,
692
+ dismissed = currentRecord?.dismissed ?: false,
693
+ )
694
+ }
695
+
696
+ private fun getRecord(notificationId: String): AndroidOngoingNotificationRecord? =
697
+ synchronized(lock) {
698
+ getRecords()[notificationId]
699
+ }
700
+
701
+ private fun getRecords(): Map<String, AndroidOngoingNotificationRecord> = readRecords(prefs)
702
+
703
+ private fun saveRecord(record: AndroidOngoingNotificationRecord) {
704
+ synchronized(lock) {
705
+ val records = readRecords(prefs).toMutableMap()
706
+ records[record.notificationId] = record
707
+ writeRecords(prefs, records)
154
708
  }
155
- return context.applicationInfo.icon
156
709
  }
710
+
711
+ private fun removeRecord(notificationId: String) {
712
+ synchronized(lock) {
713
+ val records = readRecords(prefs).toMutableMap()
714
+ records.remove(notificationId)
715
+ writeRecords(prefs, records)
716
+ }
717
+ }
718
+
719
+ private fun clearRecords() {
720
+ synchronized(lock) {
721
+ prefs.edit().remove(KEY_RECORDS).commit()
722
+ }
723
+ }
724
+
725
+ private fun allocateNotificationId(): Int =
726
+ synchronized(lock) {
727
+ val nextId = prefs.getInt(KEY_NEXT_NOTIFICATION_ID, DEFAULT_NOTIFICATION_ID)
728
+ prefs.edit().putInt(KEY_NEXT_NOTIFICATION_ID, nextId + 1).commit()
729
+ nextId
730
+ }
157
731
  }