react-native-qpush 0.0.0-stage → 1.0.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/README.md +439 -2
- package/android/build.gradle +77 -0
- package/android/gradle.properties +5 -0
- package/android/proguard-rules.pro +5 -0
- package/android/src/main/AndroidManifest.xml +29 -0
- package/android/src/main/java/com/qpush/reactnative/QPushDeviceInfo.kt +96 -0
- package/android/src/main/java/com/qpush/reactnative/QPushEventBus.kt +153 -0
- package/android/src/main/java/com/qpush/reactnative/QPushFcm.kt +108 -0
- package/android/src/main/java/com/qpush/reactnative/QPushHttp.kt +92 -0
- package/android/src/main/java/com/qpush/reactnative/QPushImages.kt +103 -0
- package/android/src/main/java/com/qpush/reactnative/QPushLog.kt +39 -0
- package/android/src/main/java/com/qpush/reactnative/QPushMessagingService.kt +77 -0
- package/android/src/main/java/com/qpush/reactnative/QPushNotificationOpenActivity.kt +121 -0
- package/android/src/main/java/com/qpush/reactnative/QPushNotificationRenderer.kt +256 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPayload.kt +114 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPushModule.kt +368 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPushPackage.kt +15 -0
- package/android/src/main/java/com/qpush/reactnative/QPushStore.kt +108 -0
- package/android/src/main/res/values/qpush_strings.xml +6 -0
- package/app.plugin.js +1 -0
- package/ios/Extension/QPushNotificationServiceExtension.swift +261 -0
- package/ios/Extension/QPushPushExtension.podspec +34 -0
- package/ios/QPushPush.m +62 -0
- package/ios/QPushPush.swift +186 -0
- package/ios/QPushPushBridge.h +5 -0
- package/ios/QPushPushCore.swift +457 -0
- package/ios/QPushPushHooks.m +237 -0
- package/ios/QPushPushSupport.swift +249 -0
- package/ios_nse_template/Info.plist +38 -0
- package/ios_nse_template/NotificationService.entitlements +10 -0
- package/ios_nse_template/NotificationService.swift +44 -0
- package/lib/NativeQPushPush.d.ts +44 -0
- package/lib/NativeQPushPush.js +4 -0
- package/lib/QPush.d.ts +117 -0
- package/lib/QPush.js +206 -0
- package/lib/core/QPushCore.d.ts +148 -0
- package/lib/core/QPushCore.js +1387 -0
- package/lib/core/constants.d.ts +37 -0
- package/lib/core/constants.js +40 -0
- package/lib/core/events.d.ts +11 -0
- package/lib/core/events.js +40 -0
- package/lib/core/http.d.ts +41 -0
- package/lib/core/http.js +123 -0
- package/lib/core/logger.d.ts +18 -0
- package/lib/core/logger.js +43 -0
- package/lib/core/operationQueue.d.ts +117 -0
- package/lib/core/operationQueue.js +264 -0
- package/lib/core/operations.d.ts +80 -0
- package/lib/core/operations.js +88 -0
- package/lib/core/payload.d.ts +20 -0
- package/lib/core/payload.js +187 -0
- package/lib/core/platform.d.ts +72 -0
- package/lib/core/platform.js +7 -0
- package/lib/core/state.d.ts +51 -0
- package/lib/core/state.js +74 -0
- package/lib/core/storage.d.ts +34 -0
- package/lib/core/storage.js +68 -0
- package/lib/core/subscription.d.ts +67 -0
- package/lib/core/subscription.js +98 -0
- package/lib/core/tags.d.ts +39 -0
- package/lib/core/tags.js +123 -0
- package/lib/core/url.d.ts +17 -0
- package/lib/core/url.js +64 -0
- package/lib/core/uuid.d.ts +2 -0
- package/lib/core/uuid.js +18 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +15 -0
- package/lib/notification.d.ts +67 -0
- package/lib/notification.js +73 -0
- package/lib/reactNativePlatform.d.ts +3 -0
- package/lib/reactNativePlatform.js +164 -0
- package/lib/types.d.ts +157 -0
- package/lib/types.js +31 -0
- package/package.json +70 -4
- package/plugin/withQPush.js +330 -0
- package/react-native-qpush.podspec +30 -0
- package/react-native.config.js +18 -0
- package/src/NativeQPushPush.ts +57 -0
- package/src/QPush.ts +292 -0
- package/src/core/QPushCore.ts +1515 -0
- package/src/core/constants.ts +42 -0
- package/src/core/events.ts +34 -0
- package/src/core/http.ts +146 -0
- package/src/core/logger.ts +42 -0
- package/src/core/operationQueue.ts +324 -0
- package/src/core/operations.ts +166 -0
- package/src/core/payload.ts +175 -0
- package/src/core/platform.ts +84 -0
- package/src/core/state.ts +127 -0
- package/src/core/storage.ts +76 -0
- package/src/core/subscription.ts +133 -0
- package/src/core/tags.ts +116 -0
- package/src/core/url.ts +58 -0
- package/src/core/uuid.ts +13 -0
- package/src/index.ts +23 -0
- package/src/notification.ts +113 -0
- package/src/reactNativePlatform.ts +172 -0
- package/src/types.ts +174 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
package com.qpush.reactnative
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.app.PendingIntent
|
|
6
|
+
import android.content.ContentResolver
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.graphics.Bitmap
|
|
10
|
+
import android.media.RingtoneManager
|
|
11
|
+
import android.net.Uri
|
|
12
|
+
import android.os.Build
|
|
13
|
+
import android.os.Bundle
|
|
14
|
+
import androidx.core.app.NotificationCompat
|
|
15
|
+
import androidx.core.app.NotificationManagerCompat
|
|
16
|
+
import java.util.Random
|
|
17
|
+
|
|
18
|
+
/** Builds and posts notifications for QPush payloads (NotificationCompat). */
|
|
19
|
+
internal object QPushNotificationRenderer {
|
|
20
|
+
const val DEFAULT_CHANNEL_ID = "qpush_default"
|
|
21
|
+
const val NOTIFICATION_TAG = "qpush"
|
|
22
|
+
const val EXTRA_QPUSH_ID = "qpush_id"
|
|
23
|
+
|
|
24
|
+
// Trampoline intent extras.
|
|
25
|
+
const val EXTRA_PAYLOAD = "qpush_payload"
|
|
26
|
+
const val EXTRA_ACTION_ID = "qpush_action_id"
|
|
27
|
+
const val EXTRA_NOTIFICATION_ID = "qpush_android_notification_id"
|
|
28
|
+
|
|
29
|
+
private const val DEFAULT_SMALL_ICON = "ic_stat_qpush_default"
|
|
30
|
+
private const val META_DEFAULT_ACCENT_COLOR = "com.qpush.NotificationAccentColor.DEFAULT"
|
|
31
|
+
private const val LARGE_ICON_SIZE = 256
|
|
32
|
+
private const val BIG_PICTURE_SIZE = 1440
|
|
33
|
+
|
|
34
|
+
private val random = Random()
|
|
35
|
+
|
|
36
|
+
/** Renders the notification. Blocking (downloads images): call off the main thread. */
|
|
37
|
+
fun render(context: Context, payload: QPushPayload) {
|
|
38
|
+
val appContext = context.applicationContext
|
|
39
|
+
if (payload.isSilent) {
|
|
40
|
+
QPushLog.d("Notification ${payload.id} has no visible content, not displayed")
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
val manager = NotificationManagerCompat.from(appContext)
|
|
44
|
+
if (!manager.areNotificationsEnabled()) {
|
|
45
|
+
QPushLog.i("Notifications are disabled, ${payload.id} not displayed")
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
val channelId = ensureChannel(appContext, payload.channelId)
|
|
50
|
+
val notificationId = payload.androidNotificationId
|
|
51
|
+
val builder = NotificationCompat.Builder(appContext, channelId)
|
|
52
|
+
.setSmallIcon(resolveSmallIcon(appContext, payload.smallIcon))
|
|
53
|
+
.setAutoCancel(true)
|
|
54
|
+
.setShowWhen(true)
|
|
55
|
+
.setWhen(System.currentTimeMillis())
|
|
56
|
+
.setPriority(mapPriority(payload.priority))
|
|
57
|
+
.setContentIntent(openIntent(appContext, payload, null, notificationId))
|
|
58
|
+
.addExtras(Bundle().apply { putString(EXTRA_QPUSH_ID, payload.id) })
|
|
59
|
+
|
|
60
|
+
payload.title?.let { builder.setContentTitle(it) }
|
|
61
|
+
payload.body?.let { builder.setContentText(it) }
|
|
62
|
+
|
|
63
|
+
resolveAccentColor(appContext, payload.accentColor)?.let { builder.setColor(it) }
|
|
64
|
+
mapVisibility(payload.visibility)?.let { builder.setVisibility(it) }
|
|
65
|
+
payload.group?.let { builder.setGroup(it) }
|
|
66
|
+
|
|
67
|
+
// Sound / vibration (pre-Oreo; on Android 8+ the channel decides).
|
|
68
|
+
val sound = resolveSound(appContext, payload.sound)
|
|
69
|
+
if (sound != null) {
|
|
70
|
+
builder.setSound(sound)
|
|
71
|
+
builder.setDefaults(NotificationCompat.DEFAULT_VIBRATE or NotificationCompat.DEFAULT_LIGHTS)
|
|
72
|
+
} else {
|
|
73
|
+
builder.setDefaults(NotificationCompat.DEFAULT_ALL)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
val largeIcon: Bitmap? = QPushImages.load(appContext, payload.largeIcon, LARGE_ICON_SIZE)
|
|
77
|
+
largeIcon?.let { builder.setLargeIcon(it) }
|
|
78
|
+
|
|
79
|
+
val bigPicture: Bitmap? = payload.image?.let { QPushImages.load(appContext, it, BIG_PICTURE_SIZE) }
|
|
80
|
+
if (bigPicture != null) {
|
|
81
|
+
val style = NotificationCompat.BigPictureStyle().bigPicture(bigPicture)
|
|
82
|
+
payload.title?.let { style.setBigContentTitle(it) }
|
|
83
|
+
payload.body?.let { style.setSummaryText(it) }
|
|
84
|
+
if (largeIcon != null) style.bigLargeIcon(null as Bitmap?)
|
|
85
|
+
builder.setStyle(style)
|
|
86
|
+
if (largeIcon == null) builder.setLargeIcon(bigPicture)
|
|
87
|
+
} else if (!payload.body.isNullOrEmpty()) {
|
|
88
|
+
builder.setStyle(NotificationCompat.BigTextStyle().bigText(payload.body))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (button in payload.buttons.take(3)) {
|
|
92
|
+
val icon = QPushImages.resourceId(appContext, button.icon)
|
|
93
|
+
builder.addAction(
|
|
94
|
+
NotificationCompat.Action.Builder(icon, button.text, openIntent(appContext, payload, button.id, notificationId))
|
|
95
|
+
.build(),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
manager.notify(NOTIFICATION_TAG, notificationId, builder.build())
|
|
101
|
+
payload.group?.let { postGroupSummary(appContext, it, channelId, payload) }
|
|
102
|
+
QPushLog.d("Displayed notification ${payload.id}")
|
|
103
|
+
} catch (e: SecurityException) {
|
|
104
|
+
// POST_NOTIFICATIONS revoked between the check and notify().
|
|
105
|
+
QPushLog.w("Not allowed to post notifications", e)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Android 7+ only groups notifications that have a summary. */
|
|
110
|
+
private fun postGroupSummary(context: Context, group: String, channelId: String, payload: QPushPayload) {
|
|
111
|
+
if (Build.VERSION.SDK_INT < 24) return
|
|
112
|
+
val summary = NotificationCompat.Builder(context, channelId)
|
|
113
|
+
.setSmallIcon(resolveSmallIcon(context, payload.smallIcon))
|
|
114
|
+
.setGroup(group)
|
|
115
|
+
.setGroupSummary(true)
|
|
116
|
+
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
|
117
|
+
.setAutoCancel(true)
|
|
118
|
+
.setContentIntent(openIntent(context, null, null, null))
|
|
119
|
+
resolveAccentColor(context, payload.accentColor)?.let { summary.setColor(it) }
|
|
120
|
+
try {
|
|
121
|
+
NotificationManagerCompat.from(context).notify(NOTIFICATION_TAG, groupSummaryId(group), summary.build())
|
|
122
|
+
} catch (e: SecurityException) {
|
|
123
|
+
QPushLog.w("Not allowed to post the group summary", e)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fun groupSummaryId(group: String): Int = "group:$group".hashCode()
|
|
128
|
+
|
|
129
|
+
private fun openIntent(context: Context, payload: QPushPayload?, actionId: String?, notificationId: Int?): PendingIntent {
|
|
130
|
+
val intent = Intent(context, QPushNotificationOpenActivity::class.java).apply {
|
|
131
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
|
132
|
+
payload?.let { putExtra(EXTRA_PAYLOAD, it.raw) }
|
|
133
|
+
actionId?.let { putExtra(EXTRA_ACTION_ID, it) }
|
|
134
|
+
notificationId?.let { putExtra(EXTRA_NOTIFICATION_ID, it) }
|
|
135
|
+
}
|
|
136
|
+
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
|
137
|
+
if (Build.VERSION.SDK_INT >= 23) flags = flags or PendingIntent.FLAG_IMMUTABLE
|
|
138
|
+
return PendingIntent.getActivity(context, random.nextInt(), intent, flags)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Returns a usable channel id, creating `qpush_default` when needed. */
|
|
142
|
+
fun ensureChannel(context: Context, requested: String?): String {
|
|
143
|
+
if (Build.VERSION.SDK_INT < 26) return requested ?: DEFAULT_CHANNEL_ID
|
|
144
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
145
|
+
if (requested != null) {
|
|
146
|
+
if (manager.getNotificationChannel(requested) != null) return requested
|
|
147
|
+
QPushLog.w("Notification channel \"$requested\" does not exist, using $DEFAULT_CHANNEL_ID")
|
|
148
|
+
}
|
|
149
|
+
if (manager.getNotificationChannel(DEFAULT_CHANNEL_ID) == null) {
|
|
150
|
+
val channel = NotificationChannel(
|
|
151
|
+
DEFAULT_CHANNEL_ID,
|
|
152
|
+
context.getString(R.string.qpush_default_channel_name),
|
|
153
|
+
NotificationManager.IMPORTANCE_HIGH,
|
|
154
|
+
)
|
|
155
|
+
channel.enableVibration(true)
|
|
156
|
+
manager.createNotificationChannel(channel)
|
|
157
|
+
}
|
|
158
|
+
return DEFAULT_CHANNEL_ID
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Payload icon -> `ic_stat_qpush_default` -> app icon. */
|
|
162
|
+
fun resolveSmallIcon(context: Context, name: String?): Int {
|
|
163
|
+
QPushImages.resourceId(context, name).takeIf { it != 0 }?.let { return it }
|
|
164
|
+
QPushImages.resourceId(context, DEFAULT_SMALL_ICON).takeIf { it != 0 }?.let { return it }
|
|
165
|
+
val appIcon = context.applicationInfo.icon
|
|
166
|
+
return if (appIcon != 0) appIcon else android.R.drawable.ic_dialog_info
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** `FF0070F3`, `#0070F3`, ... -> ARGB int. */
|
|
170
|
+
fun parseColor(value: String?): Int? {
|
|
171
|
+
if (value.isNullOrBlank()) return null
|
|
172
|
+
var hex = value.trim().removePrefix("#")
|
|
173
|
+
if (hex.length == 6) hex = "FF$hex"
|
|
174
|
+
if (hex.length != 8) return null
|
|
175
|
+
return hex.toLongOrNull(16)?.toInt()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private fun resolveAccentColor(context: Context, value: String?): Int? =
|
|
179
|
+
parseColor(value) ?: parseColor(QPushStore.manifestString(context, META_DEFAULT_ACCENT_COLOR))
|
|
180
|
+
|
|
181
|
+
private fun resolveSound(context: Context, name: String?): Uri? {
|
|
182
|
+
if (name.isNullOrBlank()) return null
|
|
183
|
+
if (name.equals("default", ignoreCase = true)) {
|
|
184
|
+
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
|
185
|
+
}
|
|
186
|
+
val resourceName = name.substringBeforeLast('.')
|
|
187
|
+
@Suppress("DiscouragedApi")
|
|
188
|
+
val id = context.resources.getIdentifier(resourceName, "raw", context.packageName)
|
|
189
|
+
if (id == 0) return null
|
|
190
|
+
return Uri.parse("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${context.packageName}/$id")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 10 -> high, 5 -> default (protocol). */
|
|
194
|
+
private fun mapPriority(priority: Int?): Int = when {
|
|
195
|
+
priority == null -> NotificationCompat.PRIORITY_HIGH
|
|
196
|
+
priority >= 9 -> NotificationCompat.PRIORITY_HIGH
|
|
197
|
+
priority >= 5 -> NotificationCompat.PRIORITY_DEFAULT
|
|
198
|
+
priority >= 1 -> NotificationCompat.PRIORITY_LOW
|
|
199
|
+
else -> NotificationCompat.PRIORITY_MIN
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 1 public, 0 private, -1 secret. */
|
|
203
|
+
private fun mapVisibility(visibility: Int?): Int? = when (visibility) {
|
|
204
|
+
1 -> NotificationCompat.VISIBILITY_PUBLIC
|
|
205
|
+
0 -> NotificationCompat.VISIBILITY_PRIVATE
|
|
206
|
+
-1 -> NotificationCompat.VISIBILITY_SECRET
|
|
207
|
+
else -> null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Removal
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
fun clearAll(context: Context) {
|
|
215
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
216
|
+
if (Build.VERSION.SDK_INT >= 23) {
|
|
217
|
+
for (notification in manager.activeNotifications) {
|
|
218
|
+
if (notification.tag == NOTIFICATION_TAG) manager.cancel(notification.tag, notification.id)
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
manager.cancelAll()
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fun remove(context: Context, qpushId: String) {
|
|
226
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
227
|
+
if (Build.VERSION.SDK_INT >= 23) {
|
|
228
|
+
for (notification in manager.activeNotifications) {
|
|
229
|
+
if (notification.tag == NOTIFICATION_TAG &&
|
|
230
|
+
notification.notification.extras?.getString(EXTRA_QPUSH_ID) == qpushId
|
|
231
|
+
) {
|
|
232
|
+
manager.cancel(notification.tag, notification.id)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
manager.cancel(NOTIFICATION_TAG, qpushId.hashCode())
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fun removeGroup(context: Context, group: String) {
|
|
241
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
242
|
+
if (Build.VERSION.SDK_INT >= 23) {
|
|
243
|
+
for (notification in manager.activeNotifications) {
|
|
244
|
+
if (notification.tag == NOTIFICATION_TAG && notification.notification.group == group) {
|
|
245
|
+
manager.cancel(notification.tag, notification.id)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
manager.cancel(NOTIFICATION_TAG, groupSummaryId(group))
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fun cancel(context: Context, notificationId: Int) {
|
|
254
|
+
NotificationManagerCompat.from(context).cancel(NOTIFICATION_TAG, notificationId)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
package com.qpush.reactnative
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
|
|
6
|
+
/** Action button of a QPush notification. */
|
|
7
|
+
internal data class QPushButton(
|
|
8
|
+
val id: String,
|
|
9
|
+
val text: String,
|
|
10
|
+
val icon: String?,
|
|
11
|
+
val url: String?,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Decoded `data["qpush"]` JSON (see docs/sdk-protocol.md, "Push payloads").
|
|
16
|
+
* Missing keys are `null` and mean "use the default".
|
|
17
|
+
*/
|
|
18
|
+
internal data class QPushPayload(
|
|
19
|
+
val id: String,
|
|
20
|
+
val title: String?,
|
|
21
|
+
val body: String?,
|
|
22
|
+
val image: String?,
|
|
23
|
+
val largeIcon: String?,
|
|
24
|
+
val smallIcon: String?,
|
|
25
|
+
val accentColor: String?,
|
|
26
|
+
val sound: String?,
|
|
27
|
+
val channelId: String?,
|
|
28
|
+
val priority: Int?,
|
|
29
|
+
val group: String?,
|
|
30
|
+
val collapseId: String?,
|
|
31
|
+
val visibility: Int?,
|
|
32
|
+
val url: String?,
|
|
33
|
+
val buttons: List<QPushButton>,
|
|
34
|
+
/** The original JSON string, forwarded untouched to JS. */
|
|
35
|
+
val raw: String,
|
|
36
|
+
) {
|
|
37
|
+
/** Data-only payload without visible content (nothing to render). */
|
|
38
|
+
val isSilent: Boolean
|
|
39
|
+
get() = title.isNullOrEmpty() && body.isNullOrEmpty() && image.isNullOrEmpty()
|
|
40
|
+
|
|
41
|
+
/** Id used with NotificationManager: notifications sharing a collapse_id replace each other. */
|
|
42
|
+
val androidNotificationId: Int
|
|
43
|
+
get() = (collapseId ?: id).hashCode()
|
|
44
|
+
|
|
45
|
+
fun buttonUrl(actionId: String?): String? =
|
|
46
|
+
if (actionId == null) null else buttons.firstOrNull { it.id == actionId }?.url
|
|
47
|
+
|
|
48
|
+
companion object {
|
|
49
|
+
fun parse(raw: String?): QPushPayload? {
|
|
50
|
+
if (raw.isNullOrEmpty()) return null
|
|
51
|
+
val json = try {
|
|
52
|
+
JSONObject(raw)
|
|
53
|
+
} catch (e: Exception) {
|
|
54
|
+
QPushLog.w("Invalid qpush payload", e)
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
val id = json.string("id") ?: return null
|
|
58
|
+
return QPushPayload(
|
|
59
|
+
id = id,
|
|
60
|
+
title = json.string("title"),
|
|
61
|
+
body = json.string("body"),
|
|
62
|
+
image = json.string("image"),
|
|
63
|
+
largeIcon = json.string("large_icon"),
|
|
64
|
+
smallIcon = json.string("small_icon"),
|
|
65
|
+
accentColor = json.string("accent_color"),
|
|
66
|
+
sound = json.string("sound"),
|
|
67
|
+
channelId = json.string("channel_id"),
|
|
68
|
+
priority = json.int("priority"),
|
|
69
|
+
group = json.string("group"),
|
|
70
|
+
collapseId = json.string("collapse_id"),
|
|
71
|
+
visibility = json.int("visibility"),
|
|
72
|
+
url = json.string("url"),
|
|
73
|
+
buttons = parseButtons(json.opt("buttons")),
|
|
74
|
+
raw = raw,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun parseButtons(value: Any?): List<QPushButton> {
|
|
79
|
+
val array = when (value) {
|
|
80
|
+
is JSONArray -> value
|
|
81
|
+
is String -> try {
|
|
82
|
+
JSONArray(value)
|
|
83
|
+
} catch (e: Exception) {
|
|
84
|
+
return emptyList()
|
|
85
|
+
}
|
|
86
|
+
else -> return emptyList()
|
|
87
|
+
}
|
|
88
|
+
val buttons = ArrayList<QPushButton>()
|
|
89
|
+
for (i in 0 until array.length()) {
|
|
90
|
+
val entry = array.optJSONObject(i) ?: continue
|
|
91
|
+
val id = entry.string("id") ?: continue
|
|
92
|
+
val text = entry.string("text") ?: continue
|
|
93
|
+
buttons.add(QPushButton(id, text, entry.string("icon"), entry.string("url")))
|
|
94
|
+
}
|
|
95
|
+
return buttons
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** `optString` returns "null" for JSON null and "" for missing keys; normalize both to null. */
|
|
99
|
+
private fun JSONObject.string(key: String): String? {
|
|
100
|
+
if (!has(key) || isNull(key)) return null
|
|
101
|
+
val value = opt(key)?.toString() ?: return null
|
|
102
|
+
return if (value.isEmpty()) null else value
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun JSONObject.int(key: String): Int? {
|
|
106
|
+
if (!has(key) || isNull(key)) return null
|
|
107
|
+
return when (val value = opt(key)) {
|
|
108
|
+
is Number -> value.toInt()
|
|
109
|
+
is String -> value.trim().toDoubleOrNull()?.toInt()
|
|
110
|
+
else -> null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|