capacitor-push-signal 0.0.2 → 0.0.4
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 +155 -9
- package/android/build.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/com/antonseagull/cap/push/signal/PushModels.kt +24 -0
- package/android/src/main/java/com/antonseagull/cap/push/signal/PushSignalCenter.kt +337 -27
- package/android/src/main/java/com/antonseagull/cap/push/signal/PushSignalPlugin.kt +42 -5
- package/dist/docs.json +140 -8
- package/dist/esm/PushSignalError.d.ts +21 -0
- package/dist/esm/PushSignalError.js +17 -0
- package/dist/esm/PushSignalError.js.map +1 -0
- package/dist/esm/definitions.d.ts +10 -2
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/pushSignal.d.ts +2 -1
- package/dist/esm/pushSignal.js +65 -5
- package/dist/esm/pushSignal.js.map +1 -1
- package/dist/esm/types.d.ts +20 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +3 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +86 -5
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +86 -5
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/PushSignalCore/PushSignalCenter.m +12 -1
- package/ios/Sources/PushSignalPlugin/PushSignalPlugin.swift +18 -1
- package/package.json +1 -1
|
@@ -7,28 +7,61 @@ import android.app.NotificationManager
|
|
|
7
7
|
import android.app.PendingIntent
|
|
8
8
|
import android.content.Context
|
|
9
9
|
import android.content.Intent
|
|
10
|
+
import android.content.pm.PackageManager
|
|
11
|
+
import android.graphics.Bitmap
|
|
12
|
+
import android.graphics.BitmapFactory
|
|
10
13
|
import android.os.Build
|
|
11
14
|
import android.os.Bundle
|
|
12
15
|
import android.os.Handler
|
|
13
16
|
import android.os.Looper
|
|
17
|
+
import android.util.Log
|
|
14
18
|
import androidx.activity.ComponentActivity
|
|
15
19
|
import androidx.core.app.NotificationCompat
|
|
20
|
+
import com.google.android.gms.common.ConnectionResult
|
|
21
|
+
import com.google.android.gms.common.GoogleApiAvailability
|
|
16
22
|
import com.google.android.gms.tasks.Tasks
|
|
17
23
|
import com.google.firebase.FirebaseApp
|
|
18
24
|
import com.google.firebase.FirebaseOptions
|
|
19
25
|
import com.google.firebase.messaging.FirebaseMessaging
|
|
20
26
|
import com.google.firebase.messaging.RemoteMessage
|
|
27
|
+
import java.net.HttpURLConnection
|
|
28
|
+
import java.net.URL
|
|
21
29
|
import java.util.Collections
|
|
22
30
|
import java.util.UUID
|
|
23
31
|
import java.util.WeakHashMap
|
|
24
32
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
33
|
+
import java.util.concurrent.Executors
|
|
34
|
+
import java.util.concurrent.TimeUnit
|
|
25
35
|
|
|
26
36
|
internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
27
37
|
private const val EXTRA_HANDLED = "pushsignal.handled"
|
|
28
38
|
private const val CHANNEL_ID = "push_signal_default"
|
|
39
|
+
private const val IMAGE_CONNECT_TIMEOUT_MS = 8_000
|
|
40
|
+
private const val IMAGE_READ_TIMEOUT_MS = 8_000
|
|
41
|
+
private const val IMAGE_MAX_BYTES = 5L * 1024L * 1024L
|
|
42
|
+
|
|
43
|
+
private const val TOKEN_TIMEOUT_SECONDS = 10L
|
|
44
|
+
private const val TOKEN_MAX_ATTEMPTS = 5
|
|
45
|
+
private const val TOKEN_INITIAL_RETRY_DELAY_MS = 1_000L
|
|
46
|
+
private const val TOKEN_MAX_RETRY_DELAY_MS = 8_000L
|
|
47
|
+
|
|
48
|
+
private const val PROVIDER_FCM = "fcm"
|
|
49
|
+
private const val PROVIDER_HMS = "hms"
|
|
50
|
+
private const val PROVIDER_MI_PUSH = "mi_push"
|
|
51
|
+
private const val PROVIDER_OPPO_PUSH = "oppo_push"
|
|
52
|
+
private const val PROVIDER_VIVO_PUSH = "vivo_push"
|
|
53
|
+
private const val PROVIDER_MEIZU_PUSH = "meizu_push"
|
|
54
|
+
private const val PROVIDER_UNKNOWN = "unknown"
|
|
55
|
+
|
|
56
|
+
/** Service apps used to positively confirm an alternative provider on the device. */
|
|
57
|
+
private const val PACKAGE_HMS = "com.huawei.hwid"
|
|
58
|
+
private const val PACKAGE_MI_PUSH = "com.xiaomi.xmsf"
|
|
59
|
+
|
|
60
|
+
private const val TAG = "PushSignal"
|
|
29
61
|
|
|
30
62
|
private val lock = Any()
|
|
31
63
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
64
|
+
private val notifyExecutor = Executors.newSingleThreadExecutor()
|
|
32
65
|
@Volatile private var application: Application? = null
|
|
33
66
|
@Volatile private var currentActivity: Activity? = null
|
|
34
67
|
@Volatile private var onMessage: ((PushMessage) -> Unit)? = null
|
|
@@ -98,7 +131,9 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
98
131
|
|
|
99
132
|
fun fetchToken(): String {
|
|
100
133
|
val context = application
|
|
101
|
-
?: throw
|
|
134
|
+
?: throw PushSignalException("E_NOT_INITIALIZED", "PushSignal is not initialized")
|
|
135
|
+
|
|
136
|
+
ensurePlayServices(context)
|
|
102
137
|
|
|
103
138
|
try {
|
|
104
139
|
if (FirebaseApp.getApps(context).isEmpty()) {
|
|
@@ -106,29 +141,229 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
106
141
|
}
|
|
107
142
|
FirebaseApp.getInstance()
|
|
108
143
|
} catch (error: IllegalStateException) {
|
|
109
|
-
throw
|
|
144
|
+
throw PushSignalException(
|
|
145
|
+
"E_FIREBASE_CONFIG",
|
|
110
146
|
"Firebase is not configured. Call initialize({ project_id, mobilesdk_app_id, current_key, project_number }) or add google-services.json.",
|
|
111
147
|
error
|
|
112
148
|
)
|
|
113
149
|
}
|
|
114
150
|
|
|
115
|
-
val
|
|
116
|
-
val token = try {
|
|
117
|
-
Tasks.await(task)
|
|
118
|
-
} catch (error: Exception) {
|
|
119
|
-
throw IllegalStateException(
|
|
120
|
-
"Failed to get an FCM token. Call initialize({ project_id, mobilesdk_app_id, current_key, project_number }) or add google-services.json.",
|
|
121
|
-
error
|
|
122
|
-
)
|
|
123
|
-
}
|
|
151
|
+
val token = awaitTokenWithRetry(context)
|
|
124
152
|
|
|
125
153
|
if (token.isNullOrEmpty()) {
|
|
126
|
-
throw
|
|
154
|
+
throw PushSignalException("E_FCM_TOKEN", "Firebase returned an empty FCM token")
|
|
127
155
|
}
|
|
128
156
|
|
|
129
157
|
return token
|
|
130
158
|
}
|
|
131
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Fails fast with an actionable message when Google Play services cannot serve FCM
|
|
162
|
+
* (for example a China-only ROM without GMS). The message names the provider the
|
|
163
|
+
* device should use instead, so the cause is obvious in JS logs.
|
|
164
|
+
*/
|
|
165
|
+
private fun ensurePlayServices(context: Context) {
|
|
166
|
+
val status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
|
|
167
|
+
if (status == ConnectionResult.SUCCESS) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
val code = when (status) {
|
|
172
|
+
ConnectionResult.SERVICE_MISSING -> "E_GMS_MISSING"
|
|
173
|
+
ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED -> "E_GMS_UPDATE_REQUIRED"
|
|
174
|
+
ConnectionResult.SERVICE_DISABLED -> "E_GMS_DISABLED"
|
|
175
|
+
ConnectionResult.SERVICE_INVALID -> "E_GMS_INVALID"
|
|
176
|
+
else -> "E_GMS_UNAVAILABLE"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
val reason = when (status) {
|
|
180
|
+
ConnectionResult.SERVICE_MISSING ->
|
|
181
|
+
"Google Play services are missing on this device, so the standard Google push service (FCM) cannot be used."
|
|
182
|
+
ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED ->
|
|
183
|
+
"Google Play services are outdated. Update them in the Play Store and try again."
|
|
184
|
+
ConnectionResult.SERVICE_DISABLED ->
|
|
185
|
+
"Google Play services are disabled. Enable them in the device settings and try again."
|
|
186
|
+
ConnectionResult.SERVICE_INVALID ->
|
|
187
|
+
"Google Play services are invalid or corrupted on this device. Reinstalling them usually helps."
|
|
188
|
+
else ->
|
|
189
|
+
"Google Play services are unavailable (code $status), so the standard Google push service (FCM) cannot be used."
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
val diagnostics = diagnose(context)
|
|
193
|
+
val message = listOfNotNull(reason, diagnostics.hint).joinToString(" ")
|
|
194
|
+
Log.w(TAG, "Play services check failed ($code): $message")
|
|
195
|
+
throw PushSignalException(code, message)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Collects device and provider diagnostics without throwing. Used both by the
|
|
200
|
+
* `getDiagnostics` API and to enrich errors with a concrete replacement service.
|
|
201
|
+
*/
|
|
202
|
+
fun diagnose(context: Context): PushDiagnostics {
|
|
203
|
+
val status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
|
|
204
|
+
val gmsAvailable = status == ConnectionResult.SUCCESS
|
|
205
|
+
val provider = resolveProvider(context, gmsAvailable)
|
|
206
|
+
val hint = providerHint(provider, gmsAvailable)
|
|
207
|
+
|
|
208
|
+
return PushDiagnostics(
|
|
209
|
+
platform = "android",
|
|
210
|
+
gmsAvailable = gmsAvailable,
|
|
211
|
+
gmsStatus = status,
|
|
212
|
+
manufacturer = Build.MANUFACTURER ?: "",
|
|
213
|
+
brand = Build.BRAND ?: "",
|
|
214
|
+
model = Build.MODEL ?: "",
|
|
215
|
+
provider = provider.id,
|
|
216
|
+
providerName = provider.name,
|
|
217
|
+
providerInstalled = provider.installed,
|
|
218
|
+
hint = hint,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private data class ProviderInfo(
|
|
223
|
+
val id: String,
|
|
224
|
+
val name: String,
|
|
225
|
+
val installed: Boolean?,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Picks the push provider this device should use. Checks the standard Google
|
|
230
|
+
* service first, then positively confirms an installed alternative, then falls
|
|
231
|
+
* back to the manufacturer mapping. Class reflection cannot see another
|
|
232
|
+
* provider's SDK unless the host bundles it, so package checks and the
|
|
233
|
+
* manufacturer are the reliable signals.
|
|
234
|
+
*/
|
|
235
|
+
private fun resolveProvider(context: Context, gmsAvailable: Boolean): ProviderInfo {
|
|
236
|
+
if (gmsAvailable) {
|
|
237
|
+
return ProviderInfo(PROVIDER_FCM, "Firebase Cloud Messaging (FCM)", true)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (isPackageInstalled(context, PACKAGE_HMS)) {
|
|
241
|
+
return ProviderInfo(PROVIDER_HMS, "HMS Push Kit (Huawei Push)", true)
|
|
242
|
+
}
|
|
243
|
+
if (isPackageInstalled(context, PACKAGE_MI_PUSH)) {
|
|
244
|
+
return ProviderInfo(PROVIDER_MI_PUSH, "Mi Push (Xiaomi Push)", true)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
val keys = setOf(Build.MANUFACTURER, Build.BRAND)
|
|
248
|
+
.filterNotNull()
|
|
249
|
+
.map { it.uppercase() }
|
|
250
|
+
|
|
251
|
+
return when {
|
|
252
|
+
keys.any { it.contains("HUAWEI") || it.contains("HONOR") } ->
|
|
253
|
+
ProviderInfo(PROVIDER_HMS, "HMS Push Kit (Huawei Push)", false)
|
|
254
|
+
keys.any { it.contains("XIAOMI") || it.contains("REDMI") || it.contains("POCO") } ->
|
|
255
|
+
ProviderInfo(PROVIDER_MI_PUSH, "Mi Push (Xiaomi Push)", false)
|
|
256
|
+
keys.any { it.contains("OPPO") || it.contains("ONEPLUS") || it.contains("REALME") } ->
|
|
257
|
+
ProviderInfo(PROVIDER_OPPO_PUSH, "OPPO Push (HeyTap)", false)
|
|
258
|
+
keys.any { it.contains("VIVO") || it.contains("IQOO") } ->
|
|
259
|
+
ProviderInfo(PROVIDER_VIVO_PUSH, "vivo Push", false)
|
|
260
|
+
keys.any { it.contains("MEIZU") } ->
|
|
261
|
+
ProviderInfo(PROVIDER_MEIZU_PUSH, "Meizu Push", false)
|
|
262
|
+
else ->
|
|
263
|
+
ProviderInfo(PROVIDER_UNKNOWN, "unknown", null)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private fun providerHint(provider: ProviderInfo, gmsAvailable: Boolean): String? {
|
|
268
|
+
if (gmsAvailable) {
|
|
269
|
+
return null
|
|
270
|
+
}
|
|
271
|
+
val device = listOfNotNull(Build.MANUFACTURER, Build.BRAND)
|
|
272
|
+
.filter { it.isNotBlank() }
|
|
273
|
+
.distinct()
|
|
274
|
+
.joinToString("/")
|
|
275
|
+
|
|
276
|
+
return when (provider.id) {
|
|
277
|
+
PROVIDER_HMS ->
|
|
278
|
+
"This device ($device) has no usable Google Play services, so FCM will not issue a token. Use HMS Push Kit (Huawei Push) instead of the standard Google service."
|
|
279
|
+
PROVIDER_MI_PUSH ->
|
|
280
|
+
"This device ($device) has no usable Google Play services, so FCM will not issue a token. Use Mi Push (Xiaomi Push) instead of the standard Google service."
|
|
281
|
+
PROVIDER_OPPO_PUSH ->
|
|
282
|
+
"This device ($device) has no usable Google Play services, so FCM will not issue a token. Use OPPO Push (HeyTap) instead of the standard Google service."
|
|
283
|
+
PROVIDER_VIVO_PUSH ->
|
|
284
|
+
"This device ($device) has no usable Google Play services, so FCM will not issue a token. Use vivo Push instead of the standard Google service."
|
|
285
|
+
PROVIDER_MEIZU_PUSH ->
|
|
286
|
+
"This device ($device) has no usable Google Play services, so FCM will not issue a token. Use Meizu Push instead of the standard Google service."
|
|
287
|
+
else ->
|
|
288
|
+
"No usable Google Play services were found and no known push provider was detected on this ROM, so FCM cannot be used. Integrate another push provider (HMS, Mi Push, OPPO/vivo/Meizu Push)."
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun isPackageInstalled(context: Context, packageName: String): Boolean {
|
|
293
|
+
return try {
|
|
294
|
+
context.packageManager.getPackageInfo(packageName, 0)
|
|
295
|
+
true
|
|
296
|
+
} catch (_: PackageManager.NameNotFoundException) {
|
|
297
|
+
false
|
|
298
|
+
} catch (_: Exception) {
|
|
299
|
+
false
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* FCM returns SERVICE_NOT_AVAILABLE for transient conditions (no Google account yet,
|
|
305
|
+
* Play services still starting, flaky network). Xiaomi/MIUI devices hit this often,
|
|
306
|
+
* so retry with backoff before surfacing the failure.
|
|
307
|
+
*/
|
|
308
|
+
private fun awaitTokenWithRetry(context: Context): String? {
|
|
309
|
+
var delayMs = TOKEN_INITIAL_RETRY_DELAY_MS
|
|
310
|
+
var lastError: Exception? = null
|
|
311
|
+
|
|
312
|
+
for (attempt in 1..TOKEN_MAX_ATTEMPTS) {
|
|
313
|
+
try {
|
|
314
|
+
return Tasks.await(
|
|
315
|
+
FirebaseMessaging.getInstance().token,
|
|
316
|
+
TOKEN_TIMEOUT_SECONDS,
|
|
317
|
+
TimeUnit.SECONDS
|
|
318
|
+
)
|
|
319
|
+
} catch (error: Exception) {
|
|
320
|
+
lastError = error
|
|
321
|
+
if (attempt == TOKEN_MAX_ATTEMPTS || !isRetryableTokenError(error)) {
|
|
322
|
+
break
|
|
323
|
+
}
|
|
324
|
+
Log.w(TAG, "FCM token attempt $attempt/$TOKEN_MAX_ATTEMPTS failed: ${error.message}. Retrying in ${delayMs}ms")
|
|
325
|
+
try {
|
|
326
|
+
Thread.sleep(delayMs)
|
|
327
|
+
} catch (_: InterruptedException) {
|
|
328
|
+
Thread.currentThread().interrupt()
|
|
329
|
+
break
|
|
330
|
+
}
|
|
331
|
+
delayMs = (delayMs * 2).coerceAtMost(TOKEN_MAX_RETRY_DELAY_MS)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
val cause = lastError
|
|
336
|
+
val diagnostics = diagnose(context)
|
|
337
|
+
val message = listOfNotNull(
|
|
338
|
+
"Failed to get an FCM token: ${cause?.message ?: "unknown error"}.",
|
|
339
|
+
"Make sure the device is signed into a Google account, Google Play services are up to date, " +
|
|
340
|
+
"and the app is allowed to use background data.",
|
|
341
|
+
"Call initialize({ project_id, mobilesdk_app_id, current_key, project_number }) or add google-services.json.",
|
|
342
|
+
diagnostics.hint,
|
|
343
|
+
).joinToString(" ")
|
|
344
|
+
Log.w(TAG, "Failed to get an FCM token: $message")
|
|
345
|
+
throw PushSignalException("E_FCM_TOKEN", message, cause)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private fun isRetryableTokenError(error: Throwable): Boolean {
|
|
349
|
+
var current: Throwable? = error
|
|
350
|
+
while (current != null) {
|
|
351
|
+
if (current is java.io.IOException || current is java.util.concurrent.TimeoutException) {
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
val message = current.message?.uppercase() ?: ""
|
|
355
|
+
if (
|
|
356
|
+
message.contains("SERVICE_NOT_AVAILABLE") ||
|
|
357
|
+
message.contains("TIMEOUT") ||
|
|
358
|
+
message.contains("INTERNAL_SERVER_ERROR")
|
|
359
|
+
) {
|
|
360
|
+
return true
|
|
361
|
+
}
|
|
362
|
+
current = current.cause
|
|
363
|
+
}
|
|
364
|
+
return false
|
|
365
|
+
}
|
|
366
|
+
|
|
132
367
|
fun emitMessage(remoteMessage: RemoteMessage) {
|
|
133
368
|
val message = remoteMessage.toPushMessage()
|
|
134
369
|
runOnMain { deliverMessage(message) }
|
|
@@ -169,18 +404,7 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
169
404
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
|
170
405
|
?: return
|
|
171
406
|
|
|
172
|
-
|
|
173
|
-
val existing = manager.getNotificationChannel(CHANNEL_ID)
|
|
174
|
-
if (existing == null) {
|
|
175
|
-
manager.createNotificationChannel(
|
|
176
|
-
NotificationChannel(
|
|
177
|
-
CHANNEL_ID,
|
|
178
|
-
"Notifications",
|
|
179
|
-
NotificationManager.IMPORTANCE_HIGH
|
|
180
|
-
)
|
|
181
|
-
)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
407
|
+
ensureChannel(manager)
|
|
184
408
|
|
|
185
409
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
186
410
|
?: return
|
|
@@ -188,6 +412,7 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
188
412
|
launchIntent.putExtra("google.message_id", message.id ?: UUID.randomUUID().toString())
|
|
189
413
|
message.title?.let { launchIntent.putExtra("gcm.notification.title", it) }
|
|
190
414
|
message.body?.let { launchIntent.putExtra("gcm.notification.body", it) }
|
|
415
|
+
message.imageUrl?.let { launchIntent.putExtra("imageUrl", it) }
|
|
191
416
|
message.data.forEach { (key, value) ->
|
|
192
417
|
launchIntent.putExtra(key, value)
|
|
193
418
|
}
|
|
@@ -200,6 +425,42 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
200
425
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
201
426
|
)
|
|
202
427
|
|
|
428
|
+
val imageUrl = message.imageUrl
|
|
429
|
+
if (imageUrl.isNullOrBlank()) {
|
|
430
|
+
manager.notify(requestCode, buildNotification(context, message, pendingIntent, null))
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
notifyExecutor.execute {
|
|
435
|
+
val bitmap = downloadBitmap(imageUrl)
|
|
436
|
+
runOnMain {
|
|
437
|
+
manager.notify(requestCode, buildNotification(context, message, pendingIntent, bitmap))
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private fun ensureChannel(manager: NotificationManager) {
|
|
443
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
if (manager.getNotificationChannel(CHANNEL_ID) != null) {
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
manager.createNotificationChannel(
|
|
450
|
+
NotificationChannel(
|
|
451
|
+
CHANNEL_ID,
|
|
452
|
+
"Notifications",
|
|
453
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private fun buildNotification(
|
|
459
|
+
context: Context,
|
|
460
|
+
message: PushMessage,
|
|
461
|
+
pendingIntent: PendingIntent,
|
|
462
|
+
bitmap: Bitmap?,
|
|
463
|
+
): android.app.Notification {
|
|
203
464
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
204
465
|
.setSmallIcon(smallIcon(context))
|
|
205
466
|
.setContentTitle(message.title.orEmpty())
|
|
@@ -210,7 +471,48 @@ internal object PushSignalCenter : Application.ActivityLifecycleCallbacks {
|
|
|
210
471
|
.setDefaults(NotificationCompat.DEFAULT_SOUND)
|
|
211
472
|
.setNumber(1)
|
|
212
473
|
|
|
213
|
-
|
|
474
|
+
if (bitmap != null) {
|
|
475
|
+
builder
|
|
476
|
+
.setLargeIcon(bitmap)
|
|
477
|
+
.setStyle(
|
|
478
|
+
NotificationCompat.BigPictureStyle()
|
|
479
|
+
.bigPicture(bitmap)
|
|
480
|
+
.bigLargeIcon(null as Bitmap?)
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return builder.build()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private fun downloadBitmap(imageUrl: String): Bitmap? {
|
|
488
|
+
var connection: HttpURLConnection? = null
|
|
489
|
+
return try {
|
|
490
|
+
connection = (URL(imageUrl).openConnection() as HttpURLConnection).apply {
|
|
491
|
+
connectTimeout = IMAGE_CONNECT_TIMEOUT_MS
|
|
492
|
+
readTimeout = IMAGE_READ_TIMEOUT_MS
|
|
493
|
+
instanceFollowRedirects = true
|
|
494
|
+
requestMethod = "GET"
|
|
495
|
+
}
|
|
496
|
+
val code = connection.responseCode
|
|
497
|
+
if (code !in 200..299) {
|
|
498
|
+
return null
|
|
499
|
+
}
|
|
500
|
+
val length = connection.contentLengthLong
|
|
501
|
+
if (length > IMAGE_MAX_BYTES) {
|
|
502
|
+
return null
|
|
503
|
+
}
|
|
504
|
+
connection.inputStream.use { input ->
|
|
505
|
+
val bytes = input.readBytes()
|
|
506
|
+
if (bytes.size > IMAGE_MAX_BYTES) {
|
|
507
|
+
return null
|
|
508
|
+
}
|
|
509
|
+
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
510
|
+
}
|
|
511
|
+
} catch (_: Exception) {
|
|
512
|
+
null
|
|
513
|
+
} finally {
|
|
514
|
+
connection?.disconnect()
|
|
515
|
+
}
|
|
214
516
|
}
|
|
215
517
|
|
|
216
518
|
private fun smallIcon(context: Context): Int {
|
|
@@ -374,7 +676,8 @@ private fun Intent.toPushMessage(): PushMessage {
|
|
|
374
676
|
extras.getString("gcm.n.body")
|
|
375
677
|
?: extras.getString("gcm.notification.body")
|
|
376
678
|
?: extras.getString("body"),
|
|
377
|
-
data
|
|
679
|
+
data,
|
|
680
|
+
resolveImageUrl(null, data),
|
|
378
681
|
)
|
|
379
682
|
}
|
|
380
683
|
|
|
@@ -387,11 +690,18 @@ private fun AndroidFirebaseConfig.hasRequiredFields(): Boolean {
|
|
|
387
690
|
|
|
388
691
|
private const val EXTRA_HANDLED_KEY = "pushsignal.handled"
|
|
389
692
|
|
|
693
|
+
internal fun resolveImageUrl(notificationImageUrl: String?, data: Map<String, String>): String? {
|
|
694
|
+
return notificationImageUrl?.takeIf { it.isNotBlank() }
|
|
695
|
+
?: data["image"]?.takeIf { it.isNotBlank() }
|
|
696
|
+
?: data["imageUrl"]?.takeIf { it.isNotBlank() }
|
|
697
|
+
}
|
|
698
|
+
|
|
390
699
|
internal fun RemoteMessage.toPushMessage(): PushMessage {
|
|
391
700
|
return PushMessage(
|
|
392
701
|
messageId,
|
|
393
702
|
notification?.title,
|
|
394
703
|
notification?.body,
|
|
395
|
-
data
|
|
704
|
+
data,
|
|
705
|
+
resolveImageUrl(notification?.imageUrl?.toString(), data),
|
|
396
706
|
)
|
|
397
707
|
}
|
|
@@ -50,7 +50,8 @@ class PushSignalPlugin : Plugin() {
|
|
|
50
50
|
if (error == null) {
|
|
51
51
|
call.resolve()
|
|
52
52
|
} else {
|
|
53
|
-
|
|
53
|
+
val code = (error as? PushSignalException)?.code ?: "E_INIT"
|
|
54
|
+
call.reject(error.message, code, error, diagnosticsData())
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -63,18 +64,38 @@ class PushSignalPlugin : Plugin() {
|
|
|
63
64
|
val token = PushSignalCenter.fetchToken()
|
|
64
65
|
val result =
|
|
65
66
|
JSObject().apply {
|
|
66
|
-
put("platform", "android")
|
|
67
67
|
put("token", token)
|
|
68
68
|
}
|
|
69
69
|
call.resolve(result)
|
|
70
70
|
} catch (error: Exception) {
|
|
71
|
-
|
|
71
|
+
val code = (error as? PushSignalException)?.code ?: "E_CREDENTIALS"
|
|
72
|
+
call.reject(error.message, code, error, diagnosticsData())
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
@PluginMethod
|
|
77
|
-
fun
|
|
78
|
+
fun getDiagnostics(call: PluginCall) {
|
|
79
|
+
executor.execute {
|
|
80
|
+
try {
|
|
81
|
+
PushSignalCenter.attach(context)
|
|
82
|
+
call.resolve(PushSignalCenter.diagnose(context).toJSObject())
|
|
83
|
+
} catch (error: Exception) {
|
|
84
|
+
call.reject(error.message, "E_DIAGNOSTICS", error)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun diagnosticsData(): JSObject? {
|
|
90
|
+
return try {
|
|
91
|
+
PushSignalCenter.diagnose(context).toJSObject()
|
|
92
|
+
} catch (_: Exception) {
|
|
93
|
+
null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@PluginMethod
|
|
98
|
+
override fun checkPermissions(call: PluginCall) {
|
|
78
99
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
79
100
|
val result = JSObject()
|
|
80
101
|
result.put("receive", "granted")
|
|
@@ -85,7 +106,7 @@ class PushSignalPlugin : Plugin() {
|
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
@PluginMethod
|
|
88
|
-
fun requestPermissions(call: PluginCall) {
|
|
109
|
+
override fun requestPermissions(call: PluginCall) {
|
|
89
110
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
|
90
111
|
getPermissionState(PUSH_NOTIFICATIONS) == PermissionState.GRANTED
|
|
91
112
|
) {
|
|
@@ -122,6 +143,7 @@ private fun PushMessage.toJSObject(): JSObject {
|
|
|
122
143
|
id?.let { map.put("id", it) }
|
|
123
144
|
title?.let { map.put("title", it) }
|
|
124
145
|
body?.let { map.put("body", it) }
|
|
146
|
+
imageUrl?.let { map.put("imageUrl", it) }
|
|
125
147
|
val dataMap = JSObject()
|
|
126
148
|
data.forEach { (key, value) ->
|
|
127
149
|
dataMap.put(key, value)
|
|
@@ -129,3 +151,18 @@ private fun PushMessage.toJSObject(): JSObject {
|
|
|
129
151
|
map.put("data", dataMap)
|
|
130
152
|
return map
|
|
131
153
|
}
|
|
154
|
+
|
|
155
|
+
private fun PushDiagnostics.toJSObject(): JSObject {
|
|
156
|
+
val map = JSObject()
|
|
157
|
+
map.put("platform", platform)
|
|
158
|
+
map.put("gmsAvailable", gmsAvailable)
|
|
159
|
+
map.put("gmsStatus", gmsStatus)
|
|
160
|
+
map.put("manufacturer", manufacturer)
|
|
161
|
+
map.put("brand", brand)
|
|
162
|
+
map.put("model", model)
|
|
163
|
+
map.put("provider", provider)
|
|
164
|
+
map.put("providerName", providerName)
|
|
165
|
+
providerInstalled?.let { map.put("providerInstalled", it) }
|
|
166
|
+
hint?.let { map.put("hint", it) }
|
|
167
|
+
return map
|
|
168
|
+
}
|