react-native-notify-kit 10.2.1 → 10.3.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/android/src/main/java/app/notifee/core/ForegroundService.java +3 -1
- package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +4 -2
- package/android/src/main/java/app/notifee/core/NotificationManager.java +28 -35
- package/android/src/main/java/app/notifee/core/database/NotifeeCoreDatabase.java +1 -4
- package/android/src/main/java/app/notifee/core/model/ChannelModel.java +8 -13
- package/android/src/main/java/app/notifee/core/model/IntervalTriggerModel.java +2 -6
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +30 -33
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java +6 -4
- package/android/src/main/java/app/notifee/core/model/TimestampTriggerModel.java +7 -4
- package/android/src/main/java/app/notifee/core/utility/BundleValueReader.java +65 -0
- package/android/src/main/java/app/notifee/core/utility/ObjectUtils.java +1 -1
- package/android/src/main/java/app/notifee/core/utility/ParcelableCompatReader.java +64 -0
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +1 -1
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeInitProvider.kt +0 -26
- package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerCurrentBehaviorTest.java +446 -0
- package/android/src/test/java/app/notifee/core/NotificationManagerCreateTriggerNotificationCurrentBehaviorTest.java +203 -0
- package/android/src/test/java/app/notifee/core/NotificationManagerPressActionOptOutTest.java +180 -0
- package/android/src/test/java/app/notifee/core/NotificationReceiverHandlerInitialNotificationTest.java +157 -0
- package/android/src/test/java/app/notifee/core/ReceiverServiceInitialNotificationTest.java +169 -0
- package/android/src/test/java/app/notifee/core/model/ChannelModelTest.java +116 -0
- package/android/src/test/java/app/notifee/core/model/IntervalTriggerModelTest.java +71 -0
- package/android/src/test/java/app/notifee/core/model/NotificationAndroidModelTest.java +309 -0
- package/android/src/test/java/app/notifee/core/model/NotificationAndroidStyleModelTest.java +154 -0
- package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +292 -6
- package/android/src/test/java/app/notifee/core/utility/BundleValueReaderTest.java +131 -0
- package/android/src/test/java/app/notifee/core/utility/ObjectUtilsTest.java +142 -0
- package/android/src/test/java/app/notifee/core/utility/ParcelableCompatReaderTest.java +124 -0
- package/android/src/test/java/io/invertase/notifee/HeadlessTaskReactHostTest.java +136 -0
- package/android/src/test/java/io/invertase/notifee/NotifeeEventSubscriberRoutingTest.java +262 -0
- package/android/src/test/java/io/invertase/notifee/NotifeeReactUtilsDrawerTest.java +24 -0
- package/dist/types/Notification.d.ts +5 -2
- package/dist/types/Notification.js +5 -2
- package/dist/types/Notification.js.map +1 -1
- package/dist/types/NotificationAndroid.d.ts +4 -0
- package/dist/types/NotificationAndroid.js +4 -0
- package/dist/types/NotificationAndroid.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/types/Notification.ts +5 -2
- package/src/types/NotificationAndroid.ts +4 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
package app.notifee.core;
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2016-present Invertase Limited & Contributors
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this library except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import static org.junit.Assert.assertEquals;
|
|
20
|
+
import static org.junit.Assert.assertNotNull;
|
|
21
|
+
import static org.junit.Assert.assertNull;
|
|
22
|
+
|
|
23
|
+
import android.Manifest;
|
|
24
|
+
import android.app.Notification;
|
|
25
|
+
import android.app.NotificationChannel;
|
|
26
|
+
import android.content.Context;
|
|
27
|
+
import android.os.Bundle;
|
|
28
|
+
import app.notifee.core.model.NotificationModel;
|
|
29
|
+
import com.google.common.util.concurrent.ListenableFuture;
|
|
30
|
+
import java.util.ArrayList;
|
|
31
|
+
import java.util.concurrent.TimeUnit;
|
|
32
|
+
import org.junit.Before;
|
|
33
|
+
import org.junit.Test;
|
|
34
|
+
import org.junit.runner.RunWith;
|
|
35
|
+
import org.robolectric.RobolectricTestRunner;
|
|
36
|
+
import org.robolectric.RuntimeEnvironment;
|
|
37
|
+
import org.robolectric.Shadows;
|
|
38
|
+
import org.robolectric.annotation.Config;
|
|
39
|
+
import org.robolectric.shadows.ShadowNotificationManager;
|
|
40
|
+
|
|
41
|
+
@RunWith(RobolectricTestRunner.class)
|
|
42
|
+
@Config(sdk = 34)
|
|
43
|
+
public class NotificationManagerPressActionOptOutTest {
|
|
44
|
+
private static final String CHANNEL_ID = "press-action-opt-out-test-channel";
|
|
45
|
+
|
|
46
|
+
@Before
|
|
47
|
+
public void setUp() {
|
|
48
|
+
Context context = RuntimeEnvironment.getApplication();
|
|
49
|
+
ContextHolder.setApplicationContext(context);
|
|
50
|
+
Shadows.shadowOf(RuntimeEnvironment.getApplication())
|
|
51
|
+
.grantPermissions(Manifest.permission.POST_NOTIFICATIONS);
|
|
52
|
+
createChannel(context);
|
|
53
|
+
|
|
54
|
+
android.app.NotificationManager notificationManager =
|
|
55
|
+
(android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
56
|
+
notificationManager.cancelAll();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
public void displayNotification_optOutPressActionOnAndroid12Plus_hasNoContentIntent()
|
|
61
|
+
throws Exception {
|
|
62
|
+
Notification notification =
|
|
63
|
+
displayNotification(buildNotificationModelWithOptOutPressAction("opt-out-android-12-plus"));
|
|
64
|
+
|
|
65
|
+
assertNull(
|
|
66
|
+
"pressAction opt-out must not set a body tap content intent on Android 12+",
|
|
67
|
+
notification.contentIntent);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Test
|
|
71
|
+
@Config(sdk = 28)
|
|
72
|
+
public void displayNotification_optOutPressActionPreAndroid12_hasNoContentIntent()
|
|
73
|
+
throws Exception {
|
|
74
|
+
Notification notification =
|
|
75
|
+
displayNotification(buildNotificationModelWithOptOutPressAction("opt-out-pre-android-12"));
|
|
76
|
+
|
|
77
|
+
assertNull(
|
|
78
|
+
"pressAction opt-out must not set a body tap content intent before Android 12",
|
|
79
|
+
notification.contentIntent);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
public void displayNotification_missingPressAction_keepsContentIntent() throws Exception {
|
|
84
|
+
Notification notification = displayNotification(buildNotificationModel("missing-press-action"));
|
|
85
|
+
|
|
86
|
+
assertNotNull(
|
|
87
|
+
"missing pressAction must synthesize the default body tap content intent",
|
|
88
|
+
notification.contentIntent);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Test
|
|
92
|
+
public void displayNotification_actionButtonWithOptOutContent_keepsActionIntent()
|
|
93
|
+
throws Exception {
|
|
94
|
+
Notification notification =
|
|
95
|
+
displayNotification(buildNotificationModelWithOptOutPressActionAndAction("opt-out-action"));
|
|
96
|
+
|
|
97
|
+
assertNull(
|
|
98
|
+
"pressAction opt-out must only disable the body tap content intent",
|
|
99
|
+
notification.contentIntent);
|
|
100
|
+
assertNotNull("action array must be present", notification.actions);
|
|
101
|
+
assertEquals("one action must be built", 1, notification.actions.length);
|
|
102
|
+
assertNotNull(
|
|
103
|
+
"action button PendingIntent must remain independent of body tap opt-out",
|
|
104
|
+
notification.actions[0].actionIntent);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private static Notification displayNotification(NotificationModel notificationModel)
|
|
108
|
+
throws Exception {
|
|
109
|
+
ListenableFuture<Void> future = NotificationManager.displayNotification(notificationModel, null);
|
|
110
|
+
future.get(5, TimeUnit.SECONDS);
|
|
111
|
+
|
|
112
|
+
Context context = RuntimeEnvironment.getApplication();
|
|
113
|
+
android.app.NotificationManager notificationManager =
|
|
114
|
+
(android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
115
|
+
ShadowNotificationManager shadowNotificationManager = Shadows.shadowOf(notificationManager);
|
|
116
|
+
Notification notification =
|
|
117
|
+
shadowNotificationManager.getNotification(notificationModel.getHashCode());
|
|
118
|
+
assertNotNull("notification must be posted", notification);
|
|
119
|
+
return notification;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private static void createChannel(Context context) {
|
|
123
|
+
android.app.NotificationManager notificationManager =
|
|
124
|
+
(android.app.NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
125
|
+
if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) {
|
|
126
|
+
NotificationChannel channel =
|
|
127
|
+
new NotificationChannel(
|
|
128
|
+
CHANNEL_ID,
|
|
129
|
+
"Press action opt-out test",
|
|
130
|
+
android.app.NotificationManager.IMPORTANCE_LOW);
|
|
131
|
+
notificationManager.createNotificationChannel(channel);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private static NotificationModel buildNotificationModel(String id) {
|
|
136
|
+
return NotificationModel.fromBundle(buildNotificationBundle(id));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static NotificationModel buildNotificationModelWithOptOutPressAction(String id) {
|
|
140
|
+
Bundle notificationBundle = buildNotificationBundle(id);
|
|
141
|
+
Bundle androidBundle = notificationBundle.getBundle("android");
|
|
142
|
+
androidBundle.putBundle(
|
|
143
|
+
"pressAction", buildPressAction(NotificationPendingIntent.PRESS_ACTION_OPT_OUT_ID));
|
|
144
|
+
return NotificationModel.fromBundle(notificationBundle);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static NotificationModel buildNotificationModelWithOptOutPressActionAndAction(String id) {
|
|
148
|
+
Bundle notificationBundle = buildNotificationBundle(id);
|
|
149
|
+
Bundle androidBundle = notificationBundle.getBundle("android");
|
|
150
|
+
androidBundle.putBundle(
|
|
151
|
+
"pressAction", buildPressAction(NotificationPendingIntent.PRESS_ACTION_OPT_OUT_ID));
|
|
152
|
+
|
|
153
|
+
Bundle action = new Bundle();
|
|
154
|
+
action.putString("title", "Reply");
|
|
155
|
+
action.putBundle("pressAction", buildPressAction("reply"));
|
|
156
|
+
ArrayList<Bundle> actions = new ArrayList<>();
|
|
157
|
+
actions.add(action);
|
|
158
|
+
androidBundle.putParcelableArrayList("actions", actions);
|
|
159
|
+
|
|
160
|
+
return NotificationModel.fromBundle(notificationBundle);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private static Bundle buildNotificationBundle(String id) {
|
|
164
|
+
Bundle notificationBundle = new Bundle();
|
|
165
|
+
notificationBundle.putString("id", id);
|
|
166
|
+
notificationBundle.putString("title", "Press action opt-out " + id);
|
|
167
|
+
notificationBundle.putString("body", "Body " + id);
|
|
168
|
+
|
|
169
|
+
Bundle androidBundle = new Bundle();
|
|
170
|
+
androidBundle.putString("channelId", CHANNEL_ID);
|
|
171
|
+
notificationBundle.putBundle("android", androidBundle);
|
|
172
|
+
return notificationBundle;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private static Bundle buildPressAction(String id) {
|
|
176
|
+
Bundle pressAction = new Bundle();
|
|
177
|
+
pressAction.putString("id", id);
|
|
178
|
+
return pressAction;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
package app.notifee.core;
|
|
2
|
+
|
|
3
|
+
import static app.notifee.core.NotificationPendingIntent.EVENT_TYPE_INTENT_KEY;
|
|
4
|
+
import static app.notifee.core.NotificationPendingIntent.NOTIFICATION_ID_INTENT_KEY;
|
|
5
|
+
import static org.junit.Assert.assertEquals;
|
|
6
|
+
import static org.junit.Assert.assertNotNull;
|
|
7
|
+
|
|
8
|
+
import android.content.Context;
|
|
9
|
+
import android.content.Intent;
|
|
10
|
+
import android.os.Bundle;
|
|
11
|
+
import app.notifee.core.event.InitialNotificationEvent;
|
|
12
|
+
import app.notifee.core.event.MainComponentEvent;
|
|
13
|
+
import app.notifee.core.event.NotificationEvent;
|
|
14
|
+
import java.util.ArrayList;
|
|
15
|
+
import java.util.List;
|
|
16
|
+
import org.greenrobot.eventbus.Subscribe;
|
|
17
|
+
import org.greenrobot.eventbus.ThreadMode;
|
|
18
|
+
import org.junit.After;
|
|
19
|
+
import org.junit.Before;
|
|
20
|
+
import org.junit.Test;
|
|
21
|
+
import org.junit.runner.RunWith;
|
|
22
|
+
import org.robolectric.RobolectricTestRunner;
|
|
23
|
+
import org.robolectric.RuntimeEnvironment;
|
|
24
|
+
import org.robolectric.annotation.Config;
|
|
25
|
+
|
|
26
|
+
@RunWith(RobolectricTestRunner.class)
|
|
27
|
+
@Config(sdk = 34)
|
|
28
|
+
public class NotificationReceiverHandlerInitialNotificationTest {
|
|
29
|
+
|
|
30
|
+
private NotificationEventCapture capture;
|
|
31
|
+
private Context context;
|
|
32
|
+
|
|
33
|
+
@Before
|
|
34
|
+
public void setUp() {
|
|
35
|
+
context = RuntimeEnvironment.getApplication();
|
|
36
|
+
ContextHolder.setApplicationContext(context);
|
|
37
|
+
clearStickyEvents();
|
|
38
|
+
capture = new NotificationEventCapture();
|
|
39
|
+
EventBus.register(capture);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@After
|
|
43
|
+
public void tearDown() {
|
|
44
|
+
EventBus.unregister(capture);
|
|
45
|
+
clearStickyEvents();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
public void handleNotification_typePress_postsNotificationEventAndInitialNotification() {
|
|
50
|
+
Intent intent =
|
|
51
|
+
buildHandlerIntent(
|
|
52
|
+
NotificationEvent.TYPE_PRESS,
|
|
53
|
+
buildNotificationBundle("handler-press"),
|
|
54
|
+
buildPressActionBundle("default", "default", "HandlerMainComponent"));
|
|
55
|
+
|
|
56
|
+
NotificationReceiverHandler.handleNotification(context, intent);
|
|
57
|
+
|
|
58
|
+
assertEquals("one NotificationEvent should be posted", 1, capture.events.size());
|
|
59
|
+
NotificationEvent event = capture.events.get(0);
|
|
60
|
+
assertEquals(NotificationEvent.TYPE_PRESS, event.getType());
|
|
61
|
+
assertEquals("handler-press", event.getNotification().getId());
|
|
62
|
+
assertNotNull("PRESS extras should be preserved", event.getExtras());
|
|
63
|
+
assertPressAction(event.getExtras(), "default", "default", "HandlerMainComponent");
|
|
64
|
+
|
|
65
|
+
InitialNotificationEvent initialNotificationEvent =
|
|
66
|
+
EventBus.getStickyEvent(InitialNotificationEvent.class);
|
|
67
|
+
assertNotNull("TYPE_PRESS should post InitialNotificationEvent", initialNotificationEvent);
|
|
68
|
+
assertEquals("handler-press", initialNotificationEvent.getNotificationModel().getId());
|
|
69
|
+
assertPressAction(
|
|
70
|
+
initialNotificationEvent.getExtras(), "default", "default", "HandlerMainComponent");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
public void handleNotification_typeActionPressWithoutLaunch_postsCurrentInitialNotification() {
|
|
75
|
+
Intent intent =
|
|
76
|
+
buildHandlerIntent(
|
|
77
|
+
NotificationEvent.TYPE_ACTION_PRESS,
|
|
78
|
+
buildNotificationBundle("handler-action-no-launch"),
|
|
79
|
+
buildPressActionBundle("reply", null, null));
|
|
80
|
+
|
|
81
|
+
NotificationReceiverHandler.handleNotification(context, intent);
|
|
82
|
+
|
|
83
|
+
assertEquals("one NotificationEvent should be posted", 1, capture.events.size());
|
|
84
|
+
NotificationEvent event = capture.events.get(0);
|
|
85
|
+
assertEquals(NotificationEvent.TYPE_ACTION_PRESS, event.getType());
|
|
86
|
+
assertEquals("handler-action-no-launch", event.getNotification().getId());
|
|
87
|
+
assertNotNull("ACTION_PRESS extras should be preserved", event.getExtras());
|
|
88
|
+
assertPressAction(event.getExtras(), "reply", null, null);
|
|
89
|
+
|
|
90
|
+
InitialNotificationEvent initialNotificationEvent =
|
|
91
|
+
EventBus.getStickyEvent(InitialNotificationEvent.class);
|
|
92
|
+
assertNotNull(
|
|
93
|
+
"Android 12+ handler currently posts InitialNotificationEvent for no-launch ACTION_PRESS",
|
|
94
|
+
initialNotificationEvent);
|
|
95
|
+
assertEquals("handler-action-no-launch", initialNotificationEvent.getNotificationModel().getId());
|
|
96
|
+
assertPressAction(initialNotificationEvent.getExtras(), "reply", null, null);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static Intent buildHandlerIntent(
|
|
100
|
+
int type, Bundle notificationBundle, Bundle pressActionBundle) {
|
|
101
|
+
Intent intent = new Intent();
|
|
102
|
+
intent.putExtra(EVENT_TYPE_INTENT_KEY, type);
|
|
103
|
+
intent.putExtra(NOTIFICATION_ID_INTENT_KEY, notificationBundle.getString("id").hashCode());
|
|
104
|
+
intent.putExtra("notification", notificationBundle);
|
|
105
|
+
intent.putExtra("pressAction", pressActionBundle);
|
|
106
|
+
return intent;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static Bundle buildNotificationBundle(String id) {
|
|
110
|
+
Bundle notificationBundle = new Bundle();
|
|
111
|
+
notificationBundle.putString("id", id);
|
|
112
|
+
notificationBundle.putString("title", "Handler test " + id);
|
|
113
|
+
notificationBundle.putString("body", "Body " + id);
|
|
114
|
+
|
|
115
|
+
Bundle androidBundle = new Bundle();
|
|
116
|
+
androidBundle.putString("channelId", "handler-initial-test-channel");
|
|
117
|
+
androidBundle.putBoolean("autoCancel", false);
|
|
118
|
+
notificationBundle.putBundle("android", androidBundle);
|
|
119
|
+
return notificationBundle;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private static Bundle buildPressActionBundle(
|
|
123
|
+
String id, String launchActivity, String mainComponent) {
|
|
124
|
+
Bundle pressActionBundle = new Bundle();
|
|
125
|
+
pressActionBundle.putString("id", id);
|
|
126
|
+
if (launchActivity != null) {
|
|
127
|
+
pressActionBundle.putString("launchActivity", launchActivity);
|
|
128
|
+
}
|
|
129
|
+
if (mainComponent != null) {
|
|
130
|
+
pressActionBundle.putString("mainComponent", mainComponent);
|
|
131
|
+
}
|
|
132
|
+
return pressActionBundle;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private static void assertPressAction(
|
|
136
|
+
Bundle extras, String id, String launchActivity, String mainComponent) {
|
|
137
|
+
Bundle pressAction = extras.getBundle("pressAction");
|
|
138
|
+
assertNotNull("pressAction should be present", pressAction);
|
|
139
|
+
assertEquals(id, pressAction.getString("id"));
|
|
140
|
+
assertEquals(launchActivity, pressAction.getString("launchActivity"));
|
|
141
|
+
assertEquals(mainComponent, pressAction.getString("mainComponent"));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static void clearStickyEvents() {
|
|
145
|
+
EventBus.removeStickEvent(InitialNotificationEvent.class);
|
|
146
|
+
EventBus.removeStickEvent(MainComponentEvent.class);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public static class NotificationEventCapture {
|
|
150
|
+
final List<NotificationEvent> events = new ArrayList<>();
|
|
151
|
+
|
|
152
|
+
@Subscribe(threadMode = ThreadMode.POSTING)
|
|
153
|
+
public void onNotificationEvent(NotificationEvent event) {
|
|
154
|
+
events.add(event);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
package app.notifee.core;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertEquals;
|
|
4
|
+
import static org.junit.Assert.assertNotNull;
|
|
5
|
+
import static org.junit.Assert.assertNull;
|
|
6
|
+
|
|
7
|
+
import android.app.Activity;
|
|
8
|
+
import android.content.Context;
|
|
9
|
+
import android.content.Intent;
|
|
10
|
+
import android.os.Bundle;
|
|
11
|
+
import app.notifee.core.event.InitialNotificationEvent;
|
|
12
|
+
import app.notifee.core.event.MainComponentEvent;
|
|
13
|
+
import app.notifee.core.event.NotificationEvent;
|
|
14
|
+
import java.util.ArrayList;
|
|
15
|
+
import java.util.List;
|
|
16
|
+
import org.greenrobot.eventbus.Subscribe;
|
|
17
|
+
import org.greenrobot.eventbus.ThreadMode;
|
|
18
|
+
import org.junit.After;
|
|
19
|
+
import org.junit.Before;
|
|
20
|
+
import org.junit.Test;
|
|
21
|
+
import org.junit.runner.RunWith;
|
|
22
|
+
import org.robolectric.Robolectric;
|
|
23
|
+
import org.robolectric.RobolectricTestRunner;
|
|
24
|
+
import org.robolectric.RuntimeEnvironment;
|
|
25
|
+
import org.robolectric.android.controller.ServiceController;
|
|
26
|
+
import org.robolectric.annotation.Config;
|
|
27
|
+
|
|
28
|
+
@RunWith(RobolectricTestRunner.class)
|
|
29
|
+
@Config(sdk = 28)
|
|
30
|
+
public class ReceiverServiceInitialNotificationTest {
|
|
31
|
+
|
|
32
|
+
private NotificationEventCapture capture;
|
|
33
|
+
private ReceiverService service;
|
|
34
|
+
private ServiceController<ReceiverService> serviceController;
|
|
35
|
+
|
|
36
|
+
@Before
|
|
37
|
+
public void setUp() {
|
|
38
|
+
Context context = RuntimeEnvironment.getApplication();
|
|
39
|
+
ContextHolder.setApplicationContext(context);
|
|
40
|
+
clearStickyEvents();
|
|
41
|
+
capture = new NotificationEventCapture();
|
|
42
|
+
EventBus.register(capture);
|
|
43
|
+
serviceController = Robolectric.buildService(ReceiverService.class).create();
|
|
44
|
+
service = serviceController.get();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@After
|
|
48
|
+
public void tearDown() {
|
|
49
|
+
EventBus.unregister(capture);
|
|
50
|
+
serviceController.destroy();
|
|
51
|
+
clearStickyEvents();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test
|
|
55
|
+
public void onActionPressIntent_withoutLaunch_preservesCurrentNoInitialNotificationBehavior() {
|
|
56
|
+
Intent intent =
|
|
57
|
+
buildServiceIntent(
|
|
58
|
+
buildNotificationBundle("service-action-no-launch"),
|
|
59
|
+
buildPressActionBundle("reply", null, null));
|
|
60
|
+
|
|
61
|
+
service.onStartCommand(intent, 0, 1);
|
|
62
|
+
|
|
63
|
+
assertEquals("one NotificationEvent should be posted", 1, capture.events.size());
|
|
64
|
+
NotificationEvent event = capture.events.get(0);
|
|
65
|
+
assertEquals(NotificationEvent.TYPE_ACTION_PRESS, event.getType());
|
|
66
|
+
assertEquals("service-action-no-launch", event.getNotification().getId());
|
|
67
|
+
assertNotNull("ACTION_PRESS extras should be preserved", event.getExtras());
|
|
68
|
+
assertPressAction(event.getExtras(), "reply", null, null);
|
|
69
|
+
|
|
70
|
+
assertNull(
|
|
71
|
+
"pre-Android 12 ReceiverService currently does not post InitialNotificationEvent for"
|
|
72
|
+
+ " no-launch ACTION_PRESS",
|
|
73
|
+
EventBus.getStickyEvent(InitialNotificationEvent.class));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
public void onActionPressIntent_withLaunch_postsInitialNotificationAfterPendingIntentSend() {
|
|
78
|
+
Intent intent =
|
|
79
|
+
buildServiceIntent(
|
|
80
|
+
buildNotificationBundle("service-action-launch"),
|
|
81
|
+
buildPressActionBundle(
|
|
82
|
+
"reply-launch", TestLaunchActivity.class.getName(), "ServiceMainComponent"));
|
|
83
|
+
|
|
84
|
+
service.onStartCommand(intent, 0, 1);
|
|
85
|
+
|
|
86
|
+
assertEquals("one NotificationEvent should be posted", 1, capture.events.size());
|
|
87
|
+
NotificationEvent event = capture.events.get(0);
|
|
88
|
+
assertEquals(NotificationEvent.TYPE_ACTION_PRESS, event.getType());
|
|
89
|
+
assertEquals("service-action-launch", event.getNotification().getId());
|
|
90
|
+
assertNotNull("ACTION_PRESS extras should be preserved", event.getExtras());
|
|
91
|
+
assertPressAction(
|
|
92
|
+
event.getExtras(),
|
|
93
|
+
"reply-launch",
|
|
94
|
+
TestLaunchActivity.class.getName(),
|
|
95
|
+
"ServiceMainComponent");
|
|
96
|
+
|
|
97
|
+
InitialNotificationEvent initialNotificationEvent =
|
|
98
|
+
EventBus.getStickyEvent(InitialNotificationEvent.class);
|
|
99
|
+
assertNotNull(
|
|
100
|
+
"pre-Android 12 ReceiverService currently posts InitialNotificationEvent when ACTION_PRESS"
|
|
101
|
+
+ " launches an activity",
|
|
102
|
+
initialNotificationEvent);
|
|
103
|
+
assertEquals("service-action-launch", initialNotificationEvent.getNotificationModel().getId());
|
|
104
|
+
assertPressAction(
|
|
105
|
+
initialNotificationEvent.getExtras(),
|
|
106
|
+
"reply-launch",
|
|
107
|
+
TestLaunchActivity.class.getName(),
|
|
108
|
+
"ServiceMainComponent");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private static Intent buildServiceIntent(Bundle notificationBundle, Bundle pressActionBundle) {
|
|
112
|
+
Intent intent = new Intent(RuntimeEnvironment.getApplication(), ReceiverService.class);
|
|
113
|
+
intent.setAction(ReceiverService.ACTION_PRESS_INTENT);
|
|
114
|
+
intent.putExtra("notification", notificationBundle);
|
|
115
|
+
intent.putExtra("pressAction", pressActionBundle);
|
|
116
|
+
return intent;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private static Bundle buildNotificationBundle(String id) {
|
|
120
|
+
Bundle notificationBundle = new Bundle();
|
|
121
|
+
notificationBundle.putString("id", id);
|
|
122
|
+
notificationBundle.putString("title", "ReceiverService test " + id);
|
|
123
|
+
notificationBundle.putString("body", "Body " + id);
|
|
124
|
+
|
|
125
|
+
Bundle androidBundle = new Bundle();
|
|
126
|
+
androidBundle.putString("channelId", "receiver-service-initial-test-channel");
|
|
127
|
+
androidBundle.putBoolean("autoCancel", false);
|
|
128
|
+
notificationBundle.putBundle("android", androidBundle);
|
|
129
|
+
return notificationBundle;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private static Bundle buildPressActionBundle(
|
|
133
|
+
String id, String launchActivity, String mainComponent) {
|
|
134
|
+
Bundle pressActionBundle = new Bundle();
|
|
135
|
+
pressActionBundle.putString("id", id);
|
|
136
|
+
if (launchActivity != null) {
|
|
137
|
+
pressActionBundle.putString("launchActivity", launchActivity);
|
|
138
|
+
}
|
|
139
|
+
if (mainComponent != null) {
|
|
140
|
+
pressActionBundle.putString("mainComponent", mainComponent);
|
|
141
|
+
}
|
|
142
|
+
return pressActionBundle;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private static void assertPressAction(
|
|
146
|
+
Bundle extras, String id, String launchActivity, String mainComponent) {
|
|
147
|
+
Bundle pressAction = extras.getBundle("pressAction");
|
|
148
|
+
assertNotNull("pressAction should be present", pressAction);
|
|
149
|
+
assertEquals(id, pressAction.getString("id"));
|
|
150
|
+
assertEquals(launchActivity, pressAction.getString("launchActivity"));
|
|
151
|
+
assertEquals(mainComponent, pressAction.getString("mainComponent"));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private static void clearStickyEvents() {
|
|
155
|
+
EventBus.removeStickEvent(InitialNotificationEvent.class);
|
|
156
|
+
EventBus.removeStickEvent(MainComponentEvent.class);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public static class TestLaunchActivity extends Activity {}
|
|
160
|
+
|
|
161
|
+
public static class NotificationEventCapture {
|
|
162
|
+
final List<NotificationEvent> events = new ArrayList<>();
|
|
163
|
+
|
|
164
|
+
@Subscribe(threadMode = ThreadMode.POSTING)
|
|
165
|
+
public void onNotificationEvent(NotificationEvent event) {
|
|
166
|
+
events.add(event);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
package app.notifee.core.model;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertArrayEquals;
|
|
4
|
+
import static org.junit.Assert.assertEquals;
|
|
5
|
+
import static org.junit.Assert.assertNull;
|
|
6
|
+
import static org.junit.Assert.assertThrows;
|
|
7
|
+
|
|
8
|
+
import android.os.Bundle;
|
|
9
|
+
import androidx.core.app.NotificationCompat;
|
|
10
|
+
import androidx.core.app.NotificationManagerCompat;
|
|
11
|
+
import java.util.ArrayList;
|
|
12
|
+
import org.junit.Test;
|
|
13
|
+
import org.junit.runner.RunWith;
|
|
14
|
+
import org.robolectric.RobolectricTestRunner;
|
|
15
|
+
|
|
16
|
+
@RunWith(RobolectricTestRunner.class)
|
|
17
|
+
public class ChannelModelTest {
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
public void fields_whenMissing_returnCurrentDefaults() {
|
|
21
|
+
ChannelModel model = ChannelModel.fromBundle(new Bundle());
|
|
22
|
+
|
|
23
|
+
assertEquals(NotificationManagerCompat.IMPORTANCE_DEFAULT, model.getImportance().intValue());
|
|
24
|
+
assertEquals(NotificationCompat.VISIBILITY_PRIVATE, model.getVisibility());
|
|
25
|
+
assertArrayEquals(new long[0], model.getVibrationPattern());
|
|
26
|
+
assertNull(model.getSound());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
public void numericFields_withIntegerAndDoubleValues_preserveCurrentCoercion() {
|
|
31
|
+
Bundle integerBundle = new Bundle();
|
|
32
|
+
integerBundle.putInt("importance", NotificationManagerCompat.IMPORTANCE_HIGH);
|
|
33
|
+
integerBundle.putInt("visibility", NotificationCompat.VISIBILITY_SECRET);
|
|
34
|
+
ChannelModel integerModel = ChannelModel.fromBundle(integerBundle);
|
|
35
|
+
|
|
36
|
+
assertEquals(NotificationManagerCompat.IMPORTANCE_HIGH, integerModel.getImportance().intValue());
|
|
37
|
+
assertEquals(NotificationCompat.VISIBILITY_SECRET, integerModel.getVisibility());
|
|
38
|
+
|
|
39
|
+
Bundle doubleBundle = new Bundle();
|
|
40
|
+
doubleBundle.putDouble("importance", NotificationManagerCompat.IMPORTANCE_HIGH + 0.9d);
|
|
41
|
+
doubleBundle.putDouble("visibility", -1.9d);
|
|
42
|
+
ChannelModel doubleModel = ChannelModel.fromBundle(doubleBundle);
|
|
43
|
+
|
|
44
|
+
assertEquals(NotificationManagerCompat.IMPORTANCE_HIGH, doubleModel.getImportance().intValue());
|
|
45
|
+
assertEquals(NotificationCompat.VISIBILITY_SECRET, doubleModel.getVisibility());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
public void numericFields_withExplicitNull_currentlyCoerceToZero() {
|
|
50
|
+
Bundle bundle = new Bundle();
|
|
51
|
+
bundle.putString("importance", null);
|
|
52
|
+
bundle.putString("visibility", null);
|
|
53
|
+
ChannelModel model = ChannelModel.fromBundle(bundle);
|
|
54
|
+
|
|
55
|
+
assertEquals(0, model.getImportance().intValue());
|
|
56
|
+
assertEquals(0, model.getVisibility());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
public void numericFields_withUnsupportedTypes_currentlyThrowClassCastException() {
|
|
61
|
+
Bundle importanceBundle = new Bundle();
|
|
62
|
+
importanceBundle.putLong("importance", 4L);
|
|
63
|
+
assertThrows(
|
|
64
|
+
ClassCastException.class, () -> ChannelModel.fromBundle(importanceBundle).getImportance());
|
|
65
|
+
|
|
66
|
+
Bundle visibilityBundle = new Bundle();
|
|
67
|
+
visibilityBundle.putString("visibility", "1");
|
|
68
|
+
assertThrows(
|
|
69
|
+
ClassCastException.class, () -> ChannelModel.fromBundle(visibilityBundle).getVisibility());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test
|
|
73
|
+
public void vibrationPattern_missingEmptyIntegerAndUnsupportedValues_preserveCurrentBehavior() {
|
|
74
|
+
assertArrayEquals(new long[0], ChannelModel.fromBundle(new Bundle()).getVibrationPattern());
|
|
75
|
+
|
|
76
|
+
Bundle emptyBundle = new Bundle();
|
|
77
|
+
putRawParcelableArrayList(emptyBundle, "vibrationPattern", new ArrayList<Integer>());
|
|
78
|
+
assertArrayEquals(new long[0], ChannelModel.fromBundle(emptyBundle).getVibrationPattern());
|
|
79
|
+
|
|
80
|
+
ArrayList<Integer> vibrationPattern = new ArrayList<>();
|
|
81
|
+
vibrationPattern.add(100);
|
|
82
|
+
vibrationPattern.add(200);
|
|
83
|
+
Bundle validBundle = new Bundle();
|
|
84
|
+
putRawParcelableArrayList(validBundle, "vibrationPattern", vibrationPattern);
|
|
85
|
+
|
|
86
|
+
assertArrayEquals(new long[] {100L, 200L}, ChannelModel.fromBundle(validBundle).getVibrationPattern());
|
|
87
|
+
|
|
88
|
+
ArrayList<Object> invalidPattern = new ArrayList<>();
|
|
89
|
+
invalidPattern.add(100L);
|
|
90
|
+
Bundle invalidBundle = new Bundle();
|
|
91
|
+
putRawParcelableArrayList(invalidBundle, "vibrationPattern", invalidPattern);
|
|
92
|
+
|
|
93
|
+
assertThrows(
|
|
94
|
+
ClassCastException.class,
|
|
95
|
+
() -> ChannelModel.fromBundle(invalidBundle).getVibrationPattern());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
public void sound_missingPresentAndExplicitNull_preserveCurrentBehavior() {
|
|
100
|
+
assertNull(ChannelModel.fromBundle(new Bundle()).getSound());
|
|
101
|
+
|
|
102
|
+
Bundle soundBundle = new Bundle();
|
|
103
|
+
soundBundle.putString("sound", "default");
|
|
104
|
+
assertEquals("default", ChannelModel.fromBundle(soundBundle).getSound());
|
|
105
|
+
|
|
106
|
+
Bundle nullSoundBundle = new Bundle();
|
|
107
|
+
nullSoundBundle.putString("sound", null);
|
|
108
|
+
assertNull(ChannelModel.fromBundle(nullSoundBundle).getSound());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
112
|
+
private static void putRawParcelableArrayList(
|
|
113
|
+
Bundle bundle, String key, ArrayList<?> arrayList) {
|
|
114
|
+
bundle.putParcelableArrayList(key, (ArrayList) arrayList);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package app.notifee.core.model;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertEquals;
|
|
4
|
+
import static org.junit.Assert.assertThrows;
|
|
5
|
+
|
|
6
|
+
import android.os.Bundle;
|
|
7
|
+
import org.junit.Test;
|
|
8
|
+
import org.junit.runner.RunWith;
|
|
9
|
+
import org.robolectric.RobolectricTestRunner;
|
|
10
|
+
|
|
11
|
+
@RunWith(RobolectricTestRunner.class)
|
|
12
|
+
public class IntervalTriggerModelTest {
|
|
13
|
+
|
|
14
|
+
@Test
|
|
15
|
+
public void getInterval_returnsMinusOneWhenMissing() {
|
|
16
|
+
IntervalTriggerModel model = IntervalTriggerModel.fromBundle(new Bundle());
|
|
17
|
+
|
|
18
|
+
assertEquals(-1, model.getInterval());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test
|
|
22
|
+
public void getInterval_returnsIntegerValue() {
|
|
23
|
+
Bundle bundle = new Bundle();
|
|
24
|
+
bundle.putInt("interval", 15);
|
|
25
|
+
|
|
26
|
+
IntervalTriggerModel model = IntervalTriggerModel.fromBundle(bundle);
|
|
27
|
+
|
|
28
|
+
assertEquals(15, model.getInterval());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
public void getInterval_truncatesDoubleValue() {
|
|
33
|
+
Bundle bundle = new Bundle();
|
|
34
|
+
bundle.putDouble("interval", 15.9d);
|
|
35
|
+
|
|
36
|
+
IntervalTriggerModel model = IntervalTriggerModel.fromBundle(bundle);
|
|
37
|
+
|
|
38
|
+
assertEquals(15, model.getInterval());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Test
|
|
42
|
+
public void getInterval_returnsZeroForExplicitNull() {
|
|
43
|
+
Bundle bundle = new Bundle();
|
|
44
|
+
bundle.putString("interval", null);
|
|
45
|
+
|
|
46
|
+
IntervalTriggerModel model = IntervalTriggerModel.fromBundle(bundle);
|
|
47
|
+
|
|
48
|
+
assertEquals(0, model.getInterval());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Test
|
|
52
|
+
public void getInterval_preservesClassCastForUnsupportedNumericTypes() {
|
|
53
|
+
Bundle longBundle = new Bundle();
|
|
54
|
+
longBundle.putLong("interval", 15L);
|
|
55
|
+
assertThrows(
|
|
56
|
+
ClassCastException.class, () -> IntervalTriggerModel.fromBundle(longBundle).getInterval());
|
|
57
|
+
|
|
58
|
+
Bundle floatBundle = new Bundle();
|
|
59
|
+
floatBundle.putFloat("interval", 15.9f);
|
|
60
|
+
assertThrows(
|
|
61
|
+
ClassCastException.class, () -> IntervalTriggerModel.fromBundle(floatBundle).getInterval());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test
|
|
65
|
+
public void getInterval_preservesClassCastForString() {
|
|
66
|
+
Bundle bundle = new Bundle();
|
|
67
|
+
bundle.putString("interval", "15");
|
|
68
|
+
|
|
69
|
+
assertThrows(ClassCastException.class, () -> IntervalTriggerModel.fromBundle(bundle).getInterval());
|
|
70
|
+
}
|
|
71
|
+
}
|