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.
- package/CHANGELOG.md +25 -0
- package/android/build.gradle +1 -3
- package/android/src/main/AndroidManifest.xml +1 -3
- package/android/src/main/java/voltra/VoltraModule.kt +120 -23
- package/android/src/main/java/voltra/VoltraNotificationManager.kt +674 -100
- package/android/src/main/java/voltra/VoltraOngoingNotificationDismissedReceiver.kt +15 -0
- package/android/src/main/java/voltra/generated/ShortNames.kt +1 -0
- package/android/src/main/java/voltra/glance/GlanceFactory.kt +1 -1
- package/android/src/main/java/voltra/glance/RemoteViewsGenerator.kt +1 -1
- package/android/src/main/java/voltra/glance/StyleUtils.kt +8 -7
- package/android/src/main/java/voltra/glance/VoltraRenderContext.kt +1 -1
- package/android/src/main/java/voltra/glance/renderers/ButtonRenderers.kt +16 -18
- package/android/src/main/java/voltra/glance/renderers/ChartBitmapRenderer.kt +18 -22
- package/android/src/main/java/voltra/glance/renderers/ChartRenderers.kt +31 -17
- package/android/src/main/java/voltra/glance/renderers/ComplexRenderers.kt +8 -17
- package/android/src/main/java/voltra/glance/renderers/InputRenderers.kt +7 -6
- package/android/src/main/java/voltra/glance/renderers/LayoutRenderers.kt +1 -0
- package/android/src/main/java/voltra/glance/renderers/LazyListRenderers.kt +2 -2
- package/android/src/main/java/voltra/glance/renderers/ProgressRenderers.kt +4 -4
- package/android/src/main/java/voltra/glance/renderers/RenderCommon.kt +6 -32
- package/android/src/main/java/voltra/glance/renderers/RendererJson.kt +128 -0
- package/android/src/main/java/voltra/glance/renderers/TextAndImageRenderers.kt +9 -2
- package/android/src/main/java/voltra/glance/renderers/TextBitmapRenderer.kt +4 -1
- package/android/src/main/java/voltra/models/VoltraPayload.kt +24 -6
- package/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayload.kt +93 -0
- package/android/src/main/java/voltra/ongoingnotification/AndroidOngoingNotificationPayloadParser.kt +13 -0
- package/android/src/main/java/voltra/parsing/VoltraDecompressor.kt +10 -10
- package/android/src/main/java/voltra/parsing/VoltraPayloadParser.kt +11 -7
- package/android/src/main/java/voltra/parsing/VoltraSerializers.kt +231 -0
- package/android/src/main/java/voltra/styling/JSColorParser.kt +10 -6
- package/android/src/main/java/voltra/styling/JSStyleParser.kt +1 -1
- package/android/src/main/java/voltra/styling/StyleConverter.kt +2 -2
- package/android/src/main/java/voltra/styling/StyleModifiers.kt +10 -9
- package/android/src/main/java/voltra/styling/StyleStructures.kt +4 -6
- package/android/src/main/java/voltra/styling/VoltraColorValue.kt +101 -0
- package/android/src/main/java/voltra/widget/VoltraRefreshActionCallback.kt +1 -12
- package/android/src/main/java/voltra/widget/VoltraWidgetUpdateRequest.kt +30 -0
- package/android/src/main/java/voltra/widget/VoltraWidgetUpdateWorker.kt +1 -12
- package/build/cjs/VoltraModule.js.map +1 -1
- package/build/cjs/android/client.js +1 -1
- package/build/cjs/android/client.js.map +1 -1
- package/build/cjs/android/components/VoltraView.js +3 -77
- package/build/cjs/android/components/VoltraView.js.map +1 -1
- package/build/cjs/android/components/VoltraWidgetPreview.js +3 -30
- package/build/cjs/android/components/VoltraWidgetPreview.js.map +1 -1
- package/build/cjs/android/dynamic-colors.js +6 -0
- package/build/cjs/android/dynamic-colors.js.map +1 -0
- package/build/cjs/android/index.js +28 -4
- package/build/cjs/android/index.js.map +1 -1
- package/build/cjs/android/jsx/AreaMark.js.map +1 -1
- package/build/cjs/android/jsx/BarMark.js.map +1 -1
- package/build/cjs/android/jsx/LineMark.js.map +1 -1
- package/build/cjs/android/jsx/PointMark.js.map +1 -1
- package/build/cjs/android/jsx/RuleMark.js.map +1 -1
- package/build/cjs/android/jsx/SectorMark.js.map +1 -1
- package/build/cjs/android/jsx/props/CheckBox.js.map +1 -1
- package/build/cjs/android/jsx/props/CircleIconButton.js.map +1 -1
- package/build/cjs/android/jsx/props/CircularProgressIndicator.js.map +1 -1
- package/build/cjs/android/jsx/props/FilledButton.js.map +1 -1
- package/build/cjs/android/jsx/props/Image.js.map +1 -1
- package/build/cjs/android/jsx/props/LinearProgressIndicator.js.map +1 -1
- package/build/cjs/android/jsx/props/OutlineButton.js.map +1 -1
- package/build/cjs/android/jsx/props/RadioButton.js.map +1 -1
- package/build/cjs/android/jsx/props/Scaffold.js.map +1 -1
- package/build/cjs/android/jsx/props/SquareIconButton.js.map +1 -1
- package/build/cjs/android/jsx/props/Switch.js.map +1 -1
- package/build/cjs/android/jsx/props/TitleBar.js.map +1 -1
- package/build/cjs/android/server.js +9 -15
- package/build/cjs/android/server.js.map +1 -1
- package/build/cjs/android/styles/types.js.map +1 -1
- package/build/cjs/android/widgets/api.js +8 -152
- package/build/cjs/android/widgets/api.js.map +1 -1
- package/build/cjs/android/widgets/renderer.js +5 -53
- package/build/cjs/android/widgets/renderer.js.map +1 -1
- package/build/cjs/client.js +24 -24
- package/build/cjs/client.js.map +1 -1
- package/build/cjs/live-activity/api.js +1 -1
- package/build/cjs/live-activity/api.js.map +1 -1
- package/build/cjs/styles/index.js.map +1 -1
- package/build/cjs/styles/types.js.map +1 -1
- package/build/cjs/types.js.map +1 -1
- package/build/cjs/widget-server.js +17 -16
- package/build/cjs/widget-server.js.map +1 -1
- package/build/cjs/widgets/renderer.js +1 -1
- package/build/cjs/widgets/renderer.js.map +1 -1
- package/build/esm/VoltraModule.js.map +1 -1
- package/build/esm/android/client.js +1 -1
- package/build/esm/android/client.js.map +1 -1
- package/build/esm/android/components/VoltraView.js +1 -43
- package/build/esm/android/components/VoltraView.js.map +1 -1
- package/build/esm/android/components/VoltraWidgetPreview.js +1 -26
- package/build/esm/android/components/VoltraWidgetPreview.js.map +1 -1
- package/build/esm/android/dynamic-colors.js +2 -0
- package/build/esm/android/dynamic-colors.js.map +1 -0
- package/build/esm/android/index.js +3 -1
- package/build/esm/android/index.js.map +1 -1
- package/build/esm/android/jsx/AreaMark.js.map +1 -1
- package/build/esm/android/jsx/BarMark.js.map +1 -1
- package/build/esm/android/jsx/LineMark.js.map +1 -1
- package/build/esm/android/jsx/PointMark.js.map +1 -1
- package/build/esm/android/jsx/RuleMark.js.map +1 -1
- package/build/esm/android/jsx/SectorMark.js.map +1 -1
- package/build/esm/android/jsx/props/CheckBox.js.map +1 -1
- package/build/esm/android/jsx/props/CircleIconButton.js.map +1 -1
- package/build/esm/android/jsx/props/CircularProgressIndicator.js.map +1 -1
- package/build/esm/android/jsx/props/FilledButton.js.map +1 -1
- package/build/esm/android/jsx/props/Image.js.map +1 -1
- package/build/esm/android/jsx/props/LinearProgressIndicator.js.map +1 -1
- package/build/esm/android/jsx/props/OutlineButton.js.map +1 -1
- package/build/esm/android/jsx/props/RadioButton.js.map +1 -1
- package/build/esm/android/jsx/props/Scaffold.js.map +1 -1
- package/build/esm/android/jsx/props/SquareIconButton.js.map +1 -1
- package/build/esm/android/jsx/props/Switch.js.map +1 -1
- package/build/esm/android/jsx/props/TitleBar.js.map +1 -1
- package/build/esm/android/server.js +2 -1
- package/build/esm/android/server.js.map +1 -1
- package/build/esm/android/styles/types.js.map +1 -1
- package/build/esm/android/widgets/api.js +1 -142
- package/build/esm/android/widgets/api.js.map +1 -1
- package/build/esm/android/widgets/renderer.js +1 -50
- package/build/esm/android/widgets/renderer.js.map +1 -1
- package/build/esm/client.js +1 -1
- package/build/esm/client.js.map +1 -1
- package/build/esm/live-activity/api.js +1 -1
- package/build/esm/live-activity/api.js.map +1 -1
- package/build/esm/styles/index.js.map +1 -1
- package/build/esm/styles/types.js.map +1 -1
- package/build/esm/types.js.map +1 -1
- package/build/esm/widget-server.js +13 -12
- package/build/esm/widget-server.js.map +1 -1
- package/build/esm/widgets/renderer.js +1 -1
- package/build/esm/widgets/renderer.js.map +1 -1
- package/build/types/VoltraModule.d.ts +1 -24
- package/build/types/VoltraModule.d.ts.map +1 -1
- package/build/types/android/client.d.ts +1 -1
- package/build/types/android/components/VoltraView.d.ts +1 -28
- package/build/types/android/components/VoltraView.d.ts.map +1 -1
- package/build/types/android/components/VoltraWidgetPreview.d.ts +1 -21
- package/build/types/android/components/VoltraWidgetPreview.d.ts.map +1 -1
- package/build/types/android/dynamic-colors.d.ts +2 -0
- package/build/types/android/dynamic-colors.d.ts.map +1 -0
- package/build/types/android/index.d.ts +18 -1
- package/build/types/android/index.d.ts.map +1 -1
- package/build/types/android/jsx/AreaMark.d.ts +2 -1
- package/build/types/android/jsx/AreaMark.d.ts.map +1 -1
- package/build/types/android/jsx/BarMark.d.ts +2 -1
- package/build/types/android/jsx/BarMark.d.ts.map +1 -1
- package/build/types/android/jsx/LineMark.d.ts +2 -1
- package/build/types/android/jsx/LineMark.d.ts.map +1 -1
- package/build/types/android/jsx/PointMark.d.ts +2 -1
- package/build/types/android/jsx/PointMark.d.ts.map +1 -1
- package/build/types/android/jsx/RuleMark.d.ts +2 -1
- package/build/types/android/jsx/RuleMark.d.ts.map +1 -1
- package/build/types/android/jsx/SectorMark.d.ts +2 -1
- package/build/types/android/jsx/SectorMark.d.ts.map +1 -1
- package/build/types/android/jsx/props/CheckBox.d.ts +3 -2
- package/build/types/android/jsx/props/CheckBox.d.ts.map +1 -1
- package/build/types/android/jsx/props/CircleIconButton.d.ts +3 -2
- package/build/types/android/jsx/props/CircleIconButton.d.ts.map +1 -1
- package/build/types/android/jsx/props/CircularProgressIndicator.d.ts +2 -1
- package/build/types/android/jsx/props/CircularProgressIndicator.d.ts.map +1 -1
- package/build/types/android/jsx/props/FilledButton.d.ts +3 -2
- package/build/types/android/jsx/props/FilledButton.d.ts.map +1 -1
- package/build/types/android/jsx/props/Image.d.ts +3 -2
- package/build/types/android/jsx/props/Image.d.ts.map +1 -1
- package/build/types/android/jsx/props/LinearProgressIndicator.d.ts +3 -2
- package/build/types/android/jsx/props/LinearProgressIndicator.d.ts.map +1 -1
- package/build/types/android/jsx/props/OutlineButton.d.ts +2 -1
- package/build/types/android/jsx/props/OutlineButton.d.ts.map +1 -1
- package/build/types/android/jsx/props/RadioButton.d.ts +3 -2
- package/build/types/android/jsx/props/RadioButton.d.ts.map +1 -1
- package/build/types/android/jsx/props/Scaffold.d.ts +2 -1
- package/build/types/android/jsx/props/Scaffold.d.ts.map +1 -1
- package/build/types/android/jsx/props/SquareIconButton.d.ts +3 -2
- package/build/types/android/jsx/props/SquareIconButton.d.ts.map +1 -1
- package/build/types/android/jsx/props/Switch.d.ts +5 -4
- package/build/types/android/jsx/props/Switch.d.ts.map +1 -1
- package/build/types/android/jsx/props/TitleBar.d.ts +3 -2
- package/build/types/android/jsx/props/TitleBar.d.ts.map +1 -1
- package/build/types/android/server.d.ts +4 -1
- package/build/types/android/server.d.ts.map +1 -1
- package/build/types/android/styles/types.d.ts +5 -4
- package/build/types/android/styles/types.d.ts.map +1 -1
- package/build/types/android/widgets/api.d.ts +1 -130
- package/build/types/android/widgets/api.d.ts.map +1 -1
- package/build/types/android/widgets/renderer.d.ts +1 -21
- package/build/types/android/widgets/renderer.d.ts.map +1 -1
- package/build/types/client.d.ts +2 -2
- package/build/types/styles/index.d.ts +1 -1
- package/build/types/styles/index.d.ts.map +1 -1
- package/build/types/styles/types.d.ts +1 -1
- package/build/types/styles/types.d.ts.map +1 -1
- package/build/types/types.d.ts +2 -1
- package/build/types/types.d.ts.map +1 -1
- package/build/types/widget-server.d.ts +4 -4
- package/build/types/widget-server.d.ts.map +1 -1
- package/ios/shared/ShortNames.swift +1 -0
- package/package.json +10 -8
- package/android/src/main/java/voltra/parsing/VoltraNodeDeserializer.kt +0 -43
- package/build/cjs/android/live-update/api.js +0 -215
- package/build/cjs/android/live-update/api.js.map +0 -1
- package/build/cjs/android/live-update/renderer.js +0 -46
- package/build/cjs/android/live-update/renderer.js.map +0 -1
- package/build/cjs/android/live-update/types.js +0 -3
- package/build/cjs/android/live-update/types.js.map +0 -1
- package/build/esm/android/live-update/api.js +0 -203
- package/build/esm/android/live-update/api.js.map +0 -1
- package/build/esm/android/live-update/renderer.js +0 -41
- package/build/esm/android/live-update/renderer.js.map +0 -1
- package/build/esm/android/live-update/types.js +0 -2
- package/build/esm/android/live-update/types.js.map +0 -1
- package/build/types/android/live-update/api.d.ts +0 -126
- package/build/types/android/live-update/api.d.ts.map +0 -1
- package/build/types/android/live-update/renderer.d.ts +0 -11
- package/build/types/android/live-update/renderer.d.ts.map +0 -1
- package/build/types/android/live-update/types.d.ts +0 -100
- 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.
|
|
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.
|
|
17
|
+
import androidx.compose.ui.graphics.toArgb
|
|
9
18
|
import kotlinx.coroutines.Dispatchers
|
|
10
19
|
import kotlinx.coroutines.withContext
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
private val
|
|
26
|
-
private val
|
|
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
|
|
154
|
+
suspend fun startOngoingNotification(
|
|
29
155
|
payload: String,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
): String =
|
|
156
|
+
options: AndroidOngoingNotificationOptions,
|
|
157
|
+
): AndroidOngoingNotificationStartResult =
|
|
33
158
|
withContext(Dispatchers.Default) {
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
val
|
|
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
|
-
|
|
88
|
-
val
|
|
352
|
+
private fun createGeneratedNotificationId(): String {
|
|
353
|
+
val intId = allocateNotificationId()
|
|
354
|
+
return "ongoing-notification-$intId"
|
|
355
|
+
}
|
|
89
356
|
|
|
90
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
.
|
|
95
|
-
.setSmallIcon(getSmallIcon(voltraPayload.smallIcon))
|
|
373
|
+
val builder =
|
|
374
|
+
Builder(appContext, record.channelId)
|
|
375
|
+
.setSmallIcon(resolveSmallIcon(record.smallIcon))
|
|
96
376
|
.setOngoing(true)
|
|
97
|
-
.setOnlyAlertOnce(
|
|
98
|
-
.
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
notification.flags = notification.flags or android.app.Notification.FLAG_ONGOING_EVENT
|
|
383
|
+
payload.title?.let { builder.setContentTitle(it) }
|
|
107
384
|
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
}
|