cordova-plugin-reteno 2.0.4 → 2.2.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/docs/android.md +22 -7
- package/docs/api.md +199 -195
- package/docs/ios.md +10 -10
- package/package.json +1 -1
- package/plugin.xml +12 -2
- package/src/android/app/build.gradle +1 -1
- package/src/android/app/src/main/AndroidManifest.xml +6 -1
- package/src/android/app/src/main/java/com/reteno/plugin/RetenoNotificationGroupingRuleProvider.java +117 -0
- package/src/android/app/src/main/java/com/reteno/plugin/RetenoNotificationSummaryManager.java +262 -0
- package/src/android/app/src/main/java/com/reteno/plugin/RetenoPlugin.java +76 -2
- package/src/android/reteno.gradle +1 -1
- package/src/ios/RetenoPlugin.swift +14 -4
- package/types/index.ts +23 -3
- package/www/cordova-plugin-reteno.js +432 -385
package/docs/ios.md
CHANGED
|
@@ -68,17 +68,16 @@ target 'App' do
|
|
|
68
68
|
|
|
69
69
|
target 'NotificationServiceExtension' do
|
|
70
70
|
inherit! :search_paths
|
|
71
|
-
pod 'Reteno', '2.7.
|
|
71
|
+
pod 'Reteno', '2.7.3'
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
target 'NotificationContentExtension' do
|
|
75
75
|
inherit! :search_paths
|
|
76
|
-
pod 'Reteno', '2.7.
|
|
76
|
+
pod 'Reteno', '2.7.3'
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
|
|
82
81
|
## Add Notification Content Extension (NCE) — Images Carousel
|
|
83
82
|
|
|
84
83
|
The Notification Content Extension is required for rich push notifications with an **Images Carousel**.
|
|
@@ -86,7 +85,7 @@ It provides the `UNNotificationContentExtension` entry point that renders a cust
|
|
|
86
85
|
|
|
87
86
|
Official guide: https://docs.reteno.com/reference/ios-images-carousel
|
|
88
87
|
|
|
89
|
-
### Create the extension target
|
|
88
|
+
### Create the extension target _(manual Xcode step)_
|
|
90
89
|
|
|
91
90
|
In Xcode: **File → New → Target → Notification Content Extension**. Name it `NotificationContentExtension`.
|
|
92
91
|
|
|
@@ -130,7 +129,7 @@ Open `NotificationContentExtension/Info.plist` as source code and replace the `N
|
|
|
130
129
|
|
|
131
130
|
Make sure the NCE **Deployment Target** in Xcode matches the main app target (iOS 15.0+). A mismatch is the most common reason carousel images fail to display.
|
|
132
131
|
|
|
133
|
-
### Embed App Extensions — Copy only when installing
|
|
132
|
+
### Embed App Extensions — Copy only when installing _(manual Xcode step)_
|
|
134
133
|
|
|
135
134
|
> **This step must be done manually in Xcode** — it cannot be automated.
|
|
136
135
|
> Official guide (Troubleshooting → "Turn off Copy Only when Installing"): https://docs.reteno.com/reference/ios-images-carousel
|
|
@@ -187,12 +186,12 @@ target 'App' do
|
|
|
187
186
|
|
|
188
187
|
target 'NotificationServiceExtension' do
|
|
189
188
|
inherit! :search_paths
|
|
190
|
-
pod 'Reteno', '2.7.
|
|
189
|
+
pod 'Reteno', '2.7.3'
|
|
191
190
|
end
|
|
192
191
|
|
|
193
192
|
target 'NotificationContentExtension' do
|
|
194
193
|
inherit! :search_paths
|
|
195
|
-
pod 'Reteno', '2.7.
|
|
194
|
+
pod 'Reteno', '2.7.3'
|
|
196
195
|
end
|
|
197
196
|
end
|
|
198
197
|
```
|
|
@@ -307,6 +306,7 @@ If your app calls `FirebaseApp.configure()` without this file, Firebase will cra
|
|
|
307
306
|
Nothing to do. Firebase iOS SDK uses **method swizzling** by default — it automatically intercepts `didRegisterForRemoteNotificationsWithDeviceToken` and forwards the APNS token to Firebase without any AppDelegate code.
|
|
308
307
|
|
|
309
308
|
When `IOS_DEVICE_TOKEN_HANDLING_MODE` is set to `manual`, the plugin:
|
|
309
|
+
|
|
310
310
|
1. Calls `FirebaseApp.configure()` automatically on `init()` if not already called and `GoogleService-Info.plist` is present.
|
|
311
311
|
2. Subscribes to FCM token updates and forwards them to Reteno once Firebase is configured.
|
|
312
312
|
|
|
@@ -381,7 +381,7 @@ await RetenoPlugin.init({
|
|
|
381
381
|
});
|
|
382
382
|
|
|
383
383
|
// Runtime pause/resume
|
|
384
|
-
await RetenoPlugin.pauseInAppMessages(true);
|
|
384
|
+
await RetenoPlugin.pauseInAppMessages(true); // pause
|
|
385
385
|
await RetenoPlugin.pauseInAppMessages(false); // resume
|
|
386
386
|
```
|
|
387
387
|
|
|
@@ -403,7 +403,7 @@ Set presentation options for foreground notifications:
|
|
|
403
403
|
```js
|
|
404
404
|
await RetenoPlugin.setWillPresentNotificationOptions({
|
|
405
405
|
options: ['badge', 'sound', 'banner'],
|
|
406
|
-
emitEvent: true
|
|
406
|
+
emitEvent: true,
|
|
407
407
|
});
|
|
408
408
|
```
|
|
409
409
|
|
|
@@ -414,7 +414,7 @@ Enable a response handler for notification taps:
|
|
|
414
414
|
```js
|
|
415
415
|
await RetenoPlugin.setDidReceiveNotificationResponseHandler({
|
|
416
416
|
enabled: true,
|
|
417
|
-
emitEvent: true
|
|
417
|
+
emitEvent: true,
|
|
418
418
|
});
|
|
419
419
|
```
|
|
420
420
|
|
package/package.json
CHANGED
package/plugin.xml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<?xml version='1.0' encoding='utf-8'?>
|
|
2
|
-
<plugin id="cordova-plugin-reteno" version="2.0
|
|
2
|
+
<plugin id="cordova-plugin-reteno" version="2.2.0" xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
|
3
3
|
<name>cordova-plugin-reteno</name>
|
|
4
4
|
<engines>
|
|
5
5
|
<engine name="cordova-android" version=">=12.0.0" />
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
|
|
23
23
|
<config-file target="AndroidManifest.xml" parent="/manifest/application">
|
|
24
24
|
<meta-data android:name="com.reteno.SDK_ACCESS_KEY" android:value="$SDK_ACCESS_KEY" />
|
|
25
|
+
<provider
|
|
26
|
+
android:name="com.reteno.plugin.RetenoNotificationGroupingRuleProvider"
|
|
27
|
+
android:authorities="${applicationId}.reteno.notification-grouping-rule"
|
|
28
|
+
android:exported="false" />
|
|
25
29
|
</config-file>
|
|
26
30
|
|
|
27
31
|
<config-file target="config.xml" parent="/*">
|
|
@@ -45,6 +49,12 @@
|
|
|
45
49
|
<source-file
|
|
46
50
|
src="src/android/app/src/main/java/com/reteno/plugin/RetenoUtil.java"
|
|
47
51
|
target-dir="src/com/reteno/plugin" />
|
|
52
|
+
<source-file
|
|
53
|
+
src="src/android/app/src/main/java/com/reteno/plugin/RetenoNotificationGroupingRuleProvider.java"
|
|
54
|
+
target-dir="src/com/reteno/plugin" />
|
|
55
|
+
<source-file
|
|
56
|
+
src="src/android/app/src/main/java/com/reteno/plugin/RetenoNotificationSummaryManager.java"
|
|
57
|
+
target-dir="src/com/reteno/plugin" />
|
|
48
58
|
</platform>
|
|
49
59
|
|
|
50
60
|
<platform name="ios">
|
|
@@ -58,7 +68,7 @@
|
|
|
58
68
|
|
|
59
69
|
<podspec>
|
|
60
70
|
<pods>
|
|
61
|
-
<pod name="Reteno" spec="2.7.
|
|
71
|
+
<pod name="Reteno" spec="2.7.3" />
|
|
62
72
|
<pod name="FirebaseMessaging" />
|
|
63
73
|
</pods>
|
|
64
74
|
</podspec>
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
3
|
package="com.reteno.plugin">
|
|
4
4
|
|
|
5
|
-
<application
|
|
5
|
+
<application>
|
|
6
|
+
<provider
|
|
7
|
+
android:name="com.reteno.plugin.RetenoNotificationGroupingRuleProvider"
|
|
8
|
+
android:authorities="${applicationId}.reteno.notification-grouping-rule"
|
|
9
|
+
android:exported="false" />
|
|
10
|
+
</application>
|
|
6
11
|
|
|
7
12
|
</manifest>
|
package/src/android/app/src/main/java/com/reteno/plugin/RetenoNotificationGroupingRuleProvider.java
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
package com.reteno.plugin;
|
|
2
|
+
|
|
3
|
+
import android.content.ContentProvider;
|
|
4
|
+
import android.content.ContentValues;
|
|
5
|
+
import android.content.Context;
|
|
6
|
+
import android.content.SharedPreferences;
|
|
7
|
+
import android.database.Cursor;
|
|
8
|
+
import android.net.Uri;
|
|
9
|
+
import android.text.TextUtils;
|
|
10
|
+
|
|
11
|
+
import com.reteno.push.RetenoNotifications;
|
|
12
|
+
|
|
13
|
+
import java.util.Map;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Restores the declarative notification grouping rule before services handle a push.
|
|
17
|
+
* This keeps grouping available when the Cordova WebView has not started yet.
|
|
18
|
+
*/
|
|
19
|
+
public final class RetenoNotificationGroupingRuleProvider extends ContentProvider {
|
|
20
|
+
private static final String PREFERENCES = "com.reteno.plugin.notification-grouping-rule";
|
|
21
|
+
private static final String PAYLOAD_KEY = "payloadKey";
|
|
22
|
+
private static final String GROUP_ID = "groupId";
|
|
23
|
+
private static final String SHOW_SUMMARY = "showSummary";
|
|
24
|
+
|
|
25
|
+
@Override
|
|
26
|
+
public boolean onCreate() {
|
|
27
|
+
Context context = getContext();
|
|
28
|
+
if (context != null) {
|
|
29
|
+
restore(context);
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static void configure(Context context, String payloadKey, String groupId, boolean showSummary) {
|
|
35
|
+
SharedPreferences.Editor editor = preferences(context).edit().clear();
|
|
36
|
+
if (!TextUtils.isEmpty(payloadKey)) {
|
|
37
|
+
editor.putString(PAYLOAD_KEY, payloadKey);
|
|
38
|
+
} else if (!TextUtils.isEmpty(groupId)) {
|
|
39
|
+
editor.putString(GROUP_ID, groupId);
|
|
40
|
+
}
|
|
41
|
+
if (showSummary) {
|
|
42
|
+
editor.putBoolean(SHOW_SUMMARY, true);
|
|
43
|
+
}
|
|
44
|
+
// Persist before returning so a process restart cannot lose a just-configured rule.
|
|
45
|
+
editor.commit();
|
|
46
|
+
install(context, payloadKey, groupId, showSummary);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private static void restore(Context context) {
|
|
50
|
+
SharedPreferences preferences = preferences(context);
|
|
51
|
+
install(
|
|
52
|
+
context,
|
|
53
|
+
preferences.getString(PAYLOAD_KEY, null),
|
|
54
|
+
preferences.getString(GROUP_ID, null),
|
|
55
|
+
preferences.getBoolean(SHOW_SUMMARY, false)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Resolves the group a received push belongs to under the persisted rule. */
|
|
60
|
+
static String resolveGroup(Context context, Map<String, String> payload) {
|
|
61
|
+
SharedPreferences preferences = preferences(context);
|
|
62
|
+
String payloadKey = preferences.getString(PAYLOAD_KEY, null);
|
|
63
|
+
if (!TextUtils.isEmpty(payloadKey)) {
|
|
64
|
+
String value = payload.get(payloadKey);
|
|
65
|
+
return TextUtils.isEmpty(value) ? null : value;
|
|
66
|
+
}
|
|
67
|
+
String groupId = preferences.getString(GROUP_ID, null);
|
|
68
|
+
return TextUtils.isEmpty(groupId) ? null : groupId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private static SharedPreferences preferences(Context context) {
|
|
72
|
+
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static void install(Context context, String payloadKey, String groupId, boolean showSummary) {
|
|
76
|
+
if (!TextUtils.isEmpty(payloadKey)) {
|
|
77
|
+
RetenoNotifications.setGroupingRule((Map<String, String> payload) -> {
|
|
78
|
+
String value = payload.get(payloadKey);
|
|
79
|
+
return TextUtils.isEmpty(value) ? null : value;
|
|
80
|
+
});
|
|
81
|
+
} else if (!TextUtils.isEmpty(groupId)) {
|
|
82
|
+
RetenoNotifications.setGroupingRule((Map<String, String> payload) -> groupId);
|
|
83
|
+
} else {
|
|
84
|
+
RetenoNotifications.setGroupingRule(null);
|
|
85
|
+
}
|
|
86
|
+
RetenoNotificationSummaryManager.setEnabled(
|
|
87
|
+
context,
|
|
88
|
+
showSummary && (!TextUtils.isEmpty(payloadKey) || !TextUtils.isEmpty(groupId)),
|
|
89
|
+
!TextUtils.isEmpty(payloadKey) ? "payloadKey:" + payloadKey : "groupId:" + groupId
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Override
|
|
94
|
+
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Override
|
|
99
|
+
public String getType(Uri uri) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Override
|
|
104
|
+
public Uri insert(Uri uri, ContentValues values) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Override
|
|
109
|
+
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Override
|
|
114
|
+
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
package com.reteno.plugin;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.app.Notification;
|
|
5
|
+
import android.app.NotificationChannel;
|
|
6
|
+
import android.app.NotificationManager;
|
|
7
|
+
import android.content.Context;
|
|
8
|
+
import android.content.pm.PackageManager;
|
|
9
|
+
import android.os.Bundle;
|
|
10
|
+
import android.os.Handler;
|
|
11
|
+
import android.os.Looper;
|
|
12
|
+
import android.service.notification.StatusBarNotification;
|
|
13
|
+
|
|
14
|
+
import androidx.core.app.ActivityCompat;
|
|
15
|
+
import androidx.core.app.NotificationCompat;
|
|
16
|
+
import androidx.core.app.NotificationManagerCompat;
|
|
17
|
+
|
|
18
|
+
import com.reteno.core.util.Procedure;
|
|
19
|
+
import com.reteno.push.RetenoNotifications;
|
|
20
|
+
|
|
21
|
+
import java.util.HashMap;
|
|
22
|
+
import java.util.Map;
|
|
23
|
+
import java.util.Objects;
|
|
24
|
+
|
|
25
|
+
/** Maintains optional Android group-summary notifications for Reteno pushes. */
|
|
26
|
+
final class RetenoNotificationSummaryManager {
|
|
27
|
+
private static final String SUMMARY_CHANNEL_ID = "reteno_group_summary";
|
|
28
|
+
private static final String SUMMARY_TAG_PREFIX = "com.reteno.plugin.group-summary:";
|
|
29
|
+
private static final int SUMMARY_NOTIFICATION_ID = 1;
|
|
30
|
+
private static final int MAX_POST_ATTEMPTS = 25;
|
|
31
|
+
private static final int MAX_REMOVAL_ATTEMPTS = 10;
|
|
32
|
+
private static final long RETRY_DELAY_MS = 200L;
|
|
33
|
+
|
|
34
|
+
private static RetenoNotificationSummaryManager instance;
|
|
35
|
+
|
|
36
|
+
private final Context context;
|
|
37
|
+
private final Handler handler = new Handler(Looper.getMainLooper());
|
|
38
|
+
private final NotificationManagerCompat notificationManager;
|
|
39
|
+
|
|
40
|
+
private Procedure<Bundle> receivedListener;
|
|
41
|
+
private Procedure<Bundle> clickListener;
|
|
42
|
+
private Procedure<Bundle> closeListener;
|
|
43
|
+
private boolean enabled;
|
|
44
|
+
private String ruleIdentity;
|
|
45
|
+
|
|
46
|
+
private RetenoNotificationSummaryManager(Context context) {
|
|
47
|
+
this.context = context.getApplicationContext();
|
|
48
|
+
this.notificationManager = NotificationManagerCompat.from(this.context);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static synchronized void setEnabled(Context context, boolean enabled, String ruleIdentity) {
|
|
52
|
+
if (instance == null) {
|
|
53
|
+
instance = new RetenoNotificationSummaryManager(context);
|
|
54
|
+
}
|
|
55
|
+
if (enabled) {
|
|
56
|
+
instance.enable(ruleIdentity);
|
|
57
|
+
} else {
|
|
58
|
+
instance.disable();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private void enable(String ruleIdentity) {
|
|
63
|
+
if (enabled) {
|
|
64
|
+
if (Objects.equals(this.ruleIdentity, ruleIdentity)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// The grouping rule may have changed since we were last enabled (e.g. constant group
|
|
68
|
+
// "A" -> "B"). Group "A" will never receive another received/click/close event under
|
|
69
|
+
// the new rule, so its summary would otherwise be orphaned forever. Drop any pending
|
|
70
|
+
// retry first - it closed over the old group and would otherwise recreate a stale
|
|
71
|
+
// summary via reconcile() right after we cancel it below - then cancel every summary
|
|
72
|
+
// this manager owns; each will be rebuilt from scratch as traffic for the new rule
|
|
73
|
+
// comes in. cancelAllSummaries() only touches our own SUMMARY_TAG_PREFIX-tagged
|
|
74
|
+
// notifications, so it never disturbs a group some other feature of the host app uses.
|
|
75
|
+
handler.removeCallbacksAndMessages(null);
|
|
76
|
+
cancelAllSummaries();
|
|
77
|
+
this.ruleIdentity = ruleIdentity;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
enabled = true;
|
|
81
|
+
this.ruleIdentity = ruleIdentity;
|
|
82
|
+
createSummaryChannel();
|
|
83
|
+
|
|
84
|
+
receivedListener = this::onReceived;
|
|
85
|
+
clickListener = this::onRemoved;
|
|
86
|
+
closeListener = this::onRemoved;
|
|
87
|
+
RetenoNotifications.getReceived().addListener(receivedListener);
|
|
88
|
+
RetenoNotifications.getClick().addListener(clickListener);
|
|
89
|
+
RetenoNotifications.getClose().addListener(closeListener);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private void disable() {
|
|
93
|
+
enabled = false;
|
|
94
|
+
ruleIdentity = null;
|
|
95
|
+
handler.removeCallbacksAndMessages(null);
|
|
96
|
+
if (receivedListener != null) {
|
|
97
|
+
RetenoNotifications.getReceived().removeListener(receivedListener);
|
|
98
|
+
receivedListener = null;
|
|
99
|
+
}
|
|
100
|
+
if (clickListener != null) {
|
|
101
|
+
RetenoNotifications.getClick().removeListener(clickListener);
|
|
102
|
+
clickListener = null;
|
|
103
|
+
}
|
|
104
|
+
if (closeListener != null) {
|
|
105
|
+
RetenoNotifications.getClose().removeListener(closeListener);
|
|
106
|
+
closeListener = null;
|
|
107
|
+
}
|
|
108
|
+
cancelAllSummaries();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private void createSummaryChannel() {
|
|
112
|
+
NotificationChannel channel = new NotificationChannel(
|
|
113
|
+
SUMMARY_CHANNEL_ID,
|
|
114
|
+
"Grouped notifications summary",
|
|
115
|
+
NotificationManager.IMPORTANCE_DEFAULT
|
|
116
|
+
);
|
|
117
|
+
notificationManager.createNotificationChannel(channel);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private void onReceived(Bundle bundle) {
|
|
121
|
+
String group = resolveGroup(bundle);
|
|
122
|
+
if (group == null) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
awaitChildAndReconcile(group, notificationId(bundle), 0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private void awaitChildAndReconcile(String group, int notificationId, int attempt) {
|
|
129
|
+
if (!enabled) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (hasChild(group, notificationId) || attempt >= MAX_POST_ATTEMPTS) {
|
|
133
|
+
reconcile(group);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
handler.postDelayed(
|
|
137
|
+
() -> awaitChildAndReconcile(group, notificationId, attempt + 1),
|
|
138
|
+
RETRY_DELAY_MS
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private void onRemoved(Bundle bundle) {
|
|
143
|
+
if (!enabled) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
String group = resolveGroup(bundle);
|
|
147
|
+
if (group == null) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
awaitRemovalAndReconcile(group, notificationId(bundle), 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private void awaitRemovalAndReconcile(String group, int notificationId, int attempt) {
|
|
154
|
+
if (!enabled) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (hasChild(group, notificationId) && attempt < MAX_REMOVAL_ATTEMPTS) {
|
|
158
|
+
handler.postDelayed(
|
|
159
|
+
() -> awaitRemovalAndReconcile(group, notificationId, attempt + 1),
|
|
160
|
+
RETRY_DELAY_MS
|
|
161
|
+
);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
reconcile(group);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private String resolveGroup(Bundle bundle) {
|
|
168
|
+
return RetenoNotificationGroupingRuleProvider.resolveGroup(context, toStringMap(bundle));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private Map<String, String> toStringMap(Bundle bundle) {
|
|
172
|
+
Map<String, String> payload = new HashMap<>();
|
|
173
|
+
if (bundle == null) {
|
|
174
|
+
return payload;
|
|
175
|
+
}
|
|
176
|
+
for (String key : bundle.keySet()) {
|
|
177
|
+
Object value = bundle.get(key);
|
|
178
|
+
payload.put(key, value == null ? null : value.toString());
|
|
179
|
+
}
|
|
180
|
+
return payload;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private int notificationId(Bundle bundle) {
|
|
184
|
+
String interactionId = bundle == null ? null : bundle.getString("es_interaction_id");
|
|
185
|
+
return interactionId == null ? 1 : interactionId.hashCode();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private boolean hasChild(String group, int notificationId) {
|
|
189
|
+
for (StatusBarNotification sbn : notificationManager.getActiveNotifications()) {
|
|
190
|
+
if (sbn.getId() == notificationId && isChildInGroup(sbn.getNotification(), group)) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private void reconcile(String group) {
|
|
198
|
+
if (!enabled) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
int childCount = 0;
|
|
203
|
+
for (StatusBarNotification sbn : notificationManager.getActiveNotifications()) {
|
|
204
|
+
if (isChildInGroup(sbn.getNotification(), group)) {
|
|
205
|
+
childCount++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
String tag = summaryTag(group);
|
|
210
|
+
if (childCount < 2) {
|
|
211
|
+
notificationManager.cancel(tag, SUMMARY_NOTIFICATION_ID);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
|
215
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Notification summary = new NotificationCompat.Builder(context, SUMMARY_CHANNEL_ID)
|
|
220
|
+
.setContentTitle("New notifications")
|
|
221
|
+
.setContentText("You have " + childCount + " new notifications")
|
|
222
|
+
.setSmallIcon(resolveSmallIcon())
|
|
223
|
+
.setGroup(group)
|
|
224
|
+
.setGroupSummary(true)
|
|
225
|
+
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
|
226
|
+
.setOnlyAlertOnce(true)
|
|
227
|
+
.build();
|
|
228
|
+
|
|
229
|
+
notificationManager.notify(tag, SUMMARY_NOTIFICATION_ID, summary);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The launcher icon (context.getApplicationInfo().icon) is commonly an adaptive icon on modern
|
|
234
|
+
* apps, which is not a valid notification small icon and can make the system reject the
|
|
235
|
+
* notification. Reuse the plain vector drawable the Reteno Android SDK itself ships and falls
|
|
236
|
+
* back to (RetenoSdkPush's "reteno_default_push_icon"), which is guaranteed to be merged into
|
|
237
|
+
* any app that depends on the SDK, instead of ever touching the launcher icon.
|
|
238
|
+
*/
|
|
239
|
+
private int resolveSmallIcon() {
|
|
240
|
+
int icon = context.getResources().getIdentifier(
|
|
241
|
+
"reteno_default_push_icon", "drawable", context.getPackageName()
|
|
242
|
+
);
|
|
243
|
+
return icon != 0 ? icon : android.R.drawable.ic_dialog_info;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private boolean isChildInGroup(Notification notification, String group) {
|
|
247
|
+
return group.equals(notification.getGroup()) && !NotificationCompat.isGroupSummary(notification);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private String summaryTag(String group) {
|
|
251
|
+
return SUMMARY_TAG_PREFIX + group;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private void cancelAllSummaries() {
|
|
255
|
+
for (StatusBarNotification sbn : notificationManager.getActiveNotifications()) {
|
|
256
|
+
String tag = sbn.getTag();
|
|
257
|
+
if (tag != null && tag.startsWith(SUMMARY_TAG_PREFIX)) {
|
|
258
|
+
notificationManager.cancel(tag, sbn.getId());
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -353,6 +353,25 @@ public class RetenoPlugin extends CordovaPlugin {
|
|
|
353
353
|
return true;
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
if ("setNotificationGroupingRule".equals(action)) {
|
|
357
|
+
Activity activity = cordova.getActivity();
|
|
358
|
+
if (activity == null) {
|
|
359
|
+
callbackContext.error("Reteno Android SDK Error: Activity is null.");
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
activity.runOnUiThread(new Runnable() {
|
|
363
|
+
@Override
|
|
364
|
+
public void run() {
|
|
365
|
+
try {
|
|
366
|
+
setNotificationGroupingRule(args, callbackContext);
|
|
367
|
+
} catch (Exception e) {
|
|
368
|
+
callbackContext.error("Reteno Android SDK Error: " + e.getLocalizedMessage());
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
356
375
|
if ("getAppInboxMessages".equals(action)) {
|
|
357
376
|
cordova.getThreadPool().execute(new Runnable() {
|
|
358
377
|
@Override
|
|
@@ -608,7 +627,7 @@ public class RetenoPlugin extends CordovaPlugin {
|
|
|
608
627
|
}
|
|
609
628
|
|
|
610
629
|
/**
|
|
611
|
-
* Register notification event listeners using the SDK 2.9.
|
|
630
|
+
* Register notification event listeners using the SDK 2.9.6 EventListener API.
|
|
612
631
|
* Each listener is a direct {@link Procedure} implementation — no reflection needed.
|
|
613
632
|
*/
|
|
614
633
|
private void registerNotificationListeners() {
|
|
@@ -1181,7 +1200,7 @@ public class RetenoPlugin extends CordovaPlugin {
|
|
|
1181
1200
|
? payload.optBoolean("sessionStartEventsEnabled", legacySessionEventsEnabled)
|
|
1182
1201
|
: legacySessionEventsEnabled;
|
|
1183
1202
|
|
|
1184
|
-
// SDK
|
|
1203
|
+
// SDK default for session end is disabled unless explicitly enabled.
|
|
1185
1204
|
boolean sessionEndEventsEnabled = payload.has("sessionEndEventsEnabled")
|
|
1186
1205
|
? payload.optBoolean("sessionEndEventsEnabled", false)
|
|
1187
1206
|
: (payload.has("sessionEventsEnabled") ? legacySessionEventsEnabled : false);
|
|
@@ -1320,6 +1339,61 @@ public class RetenoPlugin extends CordovaPlugin {
|
|
|
1320
1339
|
}
|
|
1321
1340
|
}
|
|
1322
1341
|
|
|
1342
|
+
private void setNotificationGroupingRule(JSONArray args, CallbackContext callbackContext) {
|
|
1343
|
+
Object arg0 = (args == null || args.length() == 0) ? null : args.opt(0);
|
|
1344
|
+
if (arg0 instanceof JSONArray) {
|
|
1345
|
+
arg0 = ((JSONArray) arg0).opt(0);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (arg0 == null || arg0 == JSONObject.NULL) {
|
|
1349
|
+
RetenoNotificationGroupingRuleProvider.configure(
|
|
1350
|
+
cordova.getActivity().getApplicationContext(), null, null, false
|
|
1351
|
+
);
|
|
1352
|
+
callbackContext.success(1);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
if (!(arg0 instanceof JSONObject)) {
|
|
1357
|
+
callbackContext.error("Invalid argument: expected null or an object with payloadKey or groupId");
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
JSONObject config = (JSONObject) arg0;
|
|
1362
|
+
Object rawPayloadKey = config.opt("payloadKey");
|
|
1363
|
+
Object rawGroupId = config.opt("groupId");
|
|
1364
|
+
Object rawShowSummary = config.opt("showSummary");
|
|
1365
|
+
if ((rawPayloadKey != null && rawPayloadKey != JSONObject.NULL && !(rawPayloadKey instanceof String))
|
|
1366
|
+
|| (rawGroupId != null && rawGroupId != JSONObject.NULL && !(rawGroupId instanceof String))) {
|
|
1367
|
+
callbackContext.error("Invalid argument: payloadKey and groupId must be strings");
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (rawShowSummary != null && rawShowSummary != JSONObject.NULL
|
|
1371
|
+
&& !(rawShowSummary instanceof Boolean)) {
|
|
1372
|
+
callbackContext.error("Invalid argument: showSummary must be a boolean");
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
String payloadKey = rawPayloadKey instanceof String ? (String) rawPayloadKey : null;
|
|
1377
|
+
String groupId = rawGroupId instanceof String ? (String) rawGroupId : null;
|
|
1378
|
+
payloadKey = payloadKey == null ? null : payloadKey.trim();
|
|
1379
|
+
groupId = groupId == null ? null : groupId.trim();
|
|
1380
|
+
|
|
1381
|
+
boolean hasPayloadKey = !TextUtils.isEmpty(payloadKey);
|
|
1382
|
+
boolean hasGroupId = !TextUtils.isEmpty(groupId);
|
|
1383
|
+
if (hasPayloadKey == hasGroupId) {
|
|
1384
|
+
callbackContext.error("Invalid argument: provide exactly one of payloadKey or groupId");
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
RetenoNotificationGroupingRuleProvider.configure(
|
|
1389
|
+
cordova.getActivity().getApplicationContext(),
|
|
1390
|
+
hasPayloadKey ? payloadKey : null,
|
|
1391
|
+
hasGroupId ? groupId : null,
|
|
1392
|
+
rawShowSummary instanceof Boolean && (Boolean) rawShowSummary
|
|
1393
|
+
);
|
|
1394
|
+
callbackContext.success(1);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1323
1397
|
private void getAppInboxMessages(JSONArray args, CallbackContext callbackContext) throws JSONException {
|
|
1324
1398
|
if (args == null || args.length() == 0) {
|
|
1325
1399
|
callbackContext.error("Missing argument: payload");
|