@onekeyfe/react-native-jpush 6.7.0-alpha.436 → 6.7.0-alpha.445

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 CHANGED
@@ -19,10 +19,17 @@ yarn workspace @onekeyfe/react-native-jpush typecheck
19
19
  yarn workspace @onekeyfe/react-native-jpush lint
20
20
  yarn workspace @onekeyfe/react-native-jpush test
21
21
 
22
- # Native iOS & Android E2E protocol verification (uses non-production demo key 1f2027d3cfb1b51dac664c42)
22
+ # Resolve JUnit dependencies and run the core/FCM module suites
23
+ (cd example/react-native/android && ./gradlew :onekeyfe_react-native-jpush:testDebugUnitTest :onekey-jpush-fcm:testDebugUnitTest)
24
+
25
+ # macOS: native host build/protocol regressions (loopback servers and isolated RN/Android shims)
23
26
  python3 native-modules/react-native-jpush/scripts/verify_clean_sdk.py
24
27
  ```
25
28
 
29
+ Java and Objective-C host fixtures live in `tests/android` and `tests/ios`; Python only builds, runs and checks artifacts. Android host tests use real JUnit 4.13.2 from the Gradle dependency cache. The FCM Gradle suite uses Robolectric and real Firebase message/task types, with mocked external token requests. These checks do not establish physical-device or cloud delivery.
30
+
31
+ The external Jiguang sandbox register/login test is explicitly skipped by default. Set `JPUSH_LIVE_TESTS=1` when running either Android test entry to enable it; then all network/protocol/assertion failures fail the test. CI runs both host suites plus Gradle FCM tests. A manual `native-tests` workflow can enable the same cloud check with `jpush_live_cloud`.
32
+
26
33
  ## Android Google / FCM source transport
27
34
 
28
35
  The optional Android source library in `android/fcm` replaces the opaque Jiguang FCM plugin. It delegates the Google transport to the official Firebase Messaging SDK and forwards tokens and messages into this package's JCore/JPush pipeline. Link it alongside the project's autolinked JPush module (the alias below matches app-monorepo):
@@ -37,4 +44,14 @@ project(':onekey-jpush-fcm').projectDir = new File(project(':jpush-react-native'
37
44
  googleImplementation project(':onekey-jpush-fcm')
38
45
  ```
39
46
 
40
- The host retains its existing `google-services.json` and Google Services Gradle plugin. Firebase auto-init stays disabled; JPush initialization requests the token after authorization. Cloud acceptance requires the matching Firebase credentials configured in the Jiguang sender. For Firebase-managed background notification payloads, use `cn.jpush.android.intent.NOTIFICATION_OPENED` as the click action so the private JPush activity receives `JMessageExtra`.
47
+ The host retains its existing `google-services.json` and Google Services Gradle plugin. Firebase auto-init stays disabled; JPush initialization requests the token after authorization. Granting authorization after a previously unauthorized init also requests registration without reconnecting. Cloud acceptance requires the matching Firebase credentials configured in the Jiguang sender.
48
+
49
+ ### Client enforcement of stopped/authorization controls
50
+
51
+ Vendor payloads do not need to change. `FcmMessagingService.handleIntent` checks `stopPush()` and collection authorization before calling the pinned Firebase Messaging 25.0.1 receive handler. This covers Firebase's automatic background display of notification/combined payloads as well as data-only delivery. Authorized messages retain Firebase's normal dispatch and deduplication; callback-level guards remain. Blocked events are discarded and are not replayed on resume or authorization grant. Keep the receive-entry integration tests when upgrading Firebase because this override uses its internal public receive hook.
52
+
53
+ The optional Google module disables Firebase notification delegation and removes the unguarded fallback service. A private startup provider clears any existing Google Play services notification delegate on Android 10+ after an upgrade, without requesting a token or changing consent. Other delegates are preserved. The provider runs before JS initialization, including when push is stopped or consent is absent. Existing delegation is revoked when the updated app first starts; code in a never-started update cannot revoke an already-installed delegate. Direct-boot reception before user unlock is not enabled.
54
+
55
+ For vendor background notification/combined payloads, Firebase still owns display after the gate allows delivery. Their click action must remain `cn.jpush.android.intent.NOTIFICATION_OPENED` so the private JPush activity receives `JMessageExtra`. Data-only payloads continue through the native arrival/custom-message pipeline.
56
+
57
+ Device acceptance must cover background notification-only, combined and data-only payloads: normal authorized delivery, no display/callback after stop or authorization withdrawal, normal delivery after resume/regrant, and an upgrade from existing Play services delegation. Robolectric tests run Firebase's real Intent receive/display code with only its transport acknowledgement mocked; they do not establish vendor cloud or physical-device acceptance.
@@ -107,6 +107,7 @@ android {
107
107
  testOptions {
108
108
  unitTests.returnDefaultValues = true
109
109
  unitTests.all {
110
+ inputs.property("jpushLiveTests", System.getenv("JPUSH_LIVE_TESTS") ?: "0")
110
111
  testLogging {
111
112
  events "passed", "skipped", "failed", "standardOut", "standardError"
112
113
  outputs.upToDateWhen { false }
@@ -14,6 +14,10 @@ android {
14
14
  sourceCompatibility JavaVersion.VERSION_1_8
15
15
  targetCompatibility JavaVersion.VERSION_1_8
16
16
  }
17
+
18
+ testOptions {
19
+ unitTests.includeAndroidResources = true
20
+ }
17
21
  }
18
22
 
19
23
  def coreProject = rootProject.subprojects.find {
@@ -26,4 +30,7 @@ if (coreProject == null) {
26
30
  dependencies {
27
31
  implementation project(coreProject.path)
28
32
  implementation 'com.google.firebase:firebase-messaging:25.0.1'
33
+ testImplementation 'junit:junit:4.13.2'
34
+ testImplementation 'org.robolectric:robolectric:4.17'
35
+ testImplementation 'org.mockito:mockito-core:5.20.0'
29
36
  }
@@ -1,6 +1,15 @@
1
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ xmlns:tools="http://schemas.android.com/tools">
2
3
  <application>
3
4
  <meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
5
+ <meta-data android:name="firebase_messaging_notification_delegation_enabled" android:value="false" />
6
+ <provider
7
+ android:name="com.onekey.jpush.fcm.FcmInitProvider"
8
+ android:authorities="${applicationId}.onekey.fcm.init"
9
+ android:exported="false" />
10
+ <service
11
+ android:name="com.google.firebase.messaging.FirebaseMessagingService"
12
+ tools:node="remove" />
4
13
  <service android:name="com.onekey.jpush.fcm.FcmMessagingService" android:exported="false">
5
14
  <intent-filter>
6
15
  <action android:name="com.google.firebase.MESSAGING_EVENT" />
@@ -0,0 +1,25 @@
1
+ package com.onekey.jpush.fcm;
2
+
3
+ import android.app.NotificationManager;
4
+ import android.content.Context;
5
+ import android.os.Build;
6
+ import cn.jiguang.plugins.push.common.JLogger;
7
+ import cn.jpush.android.service.InitProvider;
8
+
9
+ public final class FcmInitProvider extends InitProvider {
10
+ @Override
11
+ public boolean onCreate() {
12
+ super.onCreate();
13
+ Context context = getContext();
14
+ if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
15
+ NotificationManager notifications = context.getSystemService(NotificationManager.class);
16
+ // Firebase caches delegation initialization across upgrades, so manifest opt-out alone
17
+ // does not revoke an existing Play services delegate.
18
+ if (notifications != null && "com.google.android.gms".equals(notifications.getNotificationDelegate())) {
19
+ notifications.setNotificationDelegate(null);
20
+ JLogger.i("FCM notification delegation disabled");
21
+ }
22
+ }
23
+ return true;
24
+ }
25
+ }
@@ -1,9 +1,10 @@
1
1
  package com.onekey.jpush.fcm;
2
2
 
3
+ import android.content.Intent;
3
4
  import android.os.Bundle;
5
+ import cn.jiguang.api.JCoreManager;
4
6
  import cn.jiguang.core.JCoreStorage;
5
7
  import cn.jiguang.plugins.push.common.JLogger;
6
- import cn.jpush.android.api.JPushInterface;
7
8
  import cn.jpush.android.api.JThirdPlatFormInterface;
8
9
  import cn.jpush.android.local.ActionHelper;
9
10
  import com.google.firebase.messaging.FirebaseMessagingService;
@@ -12,12 +13,23 @@ import java.util.Map;
12
13
  import org.json.JSONObject;
13
14
 
14
15
  public final class FcmMessagingService extends FirebaseMessagingService {
16
+ @Override
17
+ public void handleIntent(Intent intent) {
18
+ // Firebase may display background notifications before onMessageReceived is called.
19
+ if (JCoreStorage.isPushStopped(this) || !JCoreStorage.isCollectionAuth(this)) {
20
+ JLogger.i("FCM receive blocked by push or authorization state");
21
+ return;
22
+ }
23
+ super.handleIntent(intent);
24
+ }
25
+
15
26
  @Override
16
27
  public void onNewToken(String token) {
17
28
  if (JCoreStorage.isPushStopped(this) || !JCoreStorage.isCollectionAuth(this)) {
18
29
  return;
19
30
  }
20
- JPushInterface.init(getApplicationContext());
31
+ // Initialize the transport without fetching and submitting this token a second time.
32
+ JCoreManager.init(getApplicationContext());
21
33
  FcmPushManager.acceptToken(getApplicationContext(), token);
22
34
  }
23
35
 
@@ -0,0 +1,684 @@
1
+ package com.onekey.jpush.fcm;
2
+
3
+ import static org.junit.Assert.*;
4
+ import static org.mockito.ArgumentMatchers.*;
5
+ import static org.mockito.Mockito.*;
6
+
7
+ import android.app.ActivityManager;
8
+ import android.app.KeyguardManager;
9
+ import android.app.Notification;
10
+ import android.app.NotificationManager;
11
+ import android.content.Context;
12
+ import android.content.Intent;
13
+ import android.content.pm.ActivityInfo;
14
+ import android.content.pm.ApplicationInfo;
15
+ import android.content.pm.ResolveInfo;
16
+ import android.os.Bundle;
17
+ import android.os.Looper;
18
+ import android.os.Process;
19
+ import cn.jiguang.api.JCoreManager;
20
+ import cn.jiguang.api.utils.JCollectionAuth;
21
+ import cn.jiguang.core.JCoreCrypto;
22
+ import cn.jiguang.core.JCoreStorage;
23
+ import cn.jiguang.core.JCoreTcpEngine;
24
+ import cn.jiguang.core.JCoreWireCodec;
25
+ import cn.jiguang.plugins.push.common.JLogger;
26
+ import cn.jpush.android.api.CustomMessage;
27
+ import cn.jpush.android.api.JPushInterface;
28
+ import cn.jpush.android.api.JThirdPlatFormInterface;
29
+ import cn.jpush.android.api.NotificationMessage;
30
+ import cn.jpush.android.local.ActionHelper;
31
+ import cn.jpush.android.service.JNotifyActivity;
32
+ import cn.jpush.android.service.JPushMessageReceiver;
33
+ import com.google.android.gms.common.GoogleApiAvailabilityLight;
34
+ import com.google.android.gms.tasks.TaskCompletionSource;
35
+ import com.google.android.gms.tasks.Tasks;
36
+ import com.google.firebase.FirebaseApp;
37
+ import com.google.firebase.FirebaseOptions;
38
+ import com.google.firebase.messaging.FirebaseMessaging;
39
+ import com.google.firebase.messaging.FirebaseMessagingService;
40
+ import com.google.firebase.messaging.RemoteMessage;
41
+ import java.io.IOException;
42
+ import java.nio.charset.StandardCharsets;
43
+ import java.util.ArrayList;
44
+ import java.util.Collections;
45
+ import java.util.List;
46
+ import java.util.Map;
47
+ import org.junit.After;
48
+ import org.junit.Before;
49
+ import org.junit.Test;
50
+ import org.junit.runner.RunWith;
51
+ import org.mockito.ArgumentCaptor;
52
+ import org.mockito.MockedStatic;
53
+ import org.robolectric.Robolectric;
54
+ import org.robolectric.RobolectricTestRunner;
55
+ import org.robolectric.RuntimeEnvironment;
56
+ import org.robolectric.Shadows;
57
+ import org.robolectric.annotation.Config;
58
+ import org.robolectric.annotation.LooperMode;
59
+ import org.robolectric.util.ReflectionHelpers;
60
+
61
+ @RunWith(RobolectricTestRunner.class)
62
+ @Config(sdk = 28, manifest = Config.NONE)
63
+ @LooperMode(LooperMode.Mode.PAUSED)
64
+ public class FcmAdapterTest {
65
+ private Context context;
66
+ private FcmMessagingService service;
67
+ private NotificationManager notifications;
68
+ private Object firebaseRpc;
69
+ private final List<NotificationMessage> arrived = new ArrayList<>();
70
+ private final List<NotificationMessage> opened = new ArrayList<>();
71
+ private final List<CustomMessage> custom = new ArrayList<>();
72
+
73
+ @Before
74
+ public void setUp() throws Exception {
75
+ context = RuntimeEnvironment.getApplication();
76
+ ReflectionHelpers.setStaticField(JCoreStorage.class, "sAppContext", null);
77
+ ReflectionHelpers.<Map<String, Object>>getStaticField(JCoreStorage.class, "MEMORY_FALLBACK").clear();
78
+ JCoreStorage.setAppContext(context);
79
+ JCoreStorage.setCollectionAuth(context, true);
80
+ JCoreStorage.setPushStopped(context, false);
81
+ ReflectionHelpers.setStaticField(JThirdPlatFormInterface.class, "fcmPlugin", null);
82
+ ReflectionHelpers.setStaticField(JThirdPlatFormInterface.class, "fcmRegistered", false);
83
+ ReflectionHelpers.<java.util.Queue<String>>getStaticField(FirebaseMessagingService.class, "recentlyReceivedMessageIds").clear();
84
+ JLogger.setLoggerEnable(true);
85
+ JLogger.clearRecentLogs();
86
+ notifications = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
87
+ notifications.cancelAll();
88
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND);
89
+ ActionHelper.getInstance().clearRegisteredReceivers();
90
+ ActionHelper.getInstance().registerReceiver(new JPushMessageReceiver() {
91
+ @Override public void onNotifyMessageArrived(Context c, NotificationMessage message) { arrived.add(message); }
92
+ @Override public void onNotifyMessageOpened(Context c, NotificationMessage message) { opened.add(message); }
93
+ @Override public void onMessage(Context c, CustomMessage message) { custom.add(message); }
94
+ });
95
+ service = Robolectric.buildService(FcmMessagingService.class).create().get();
96
+ // Isolate only the SDK's transport acknowledgement; run its real receive/display code.
97
+ firebaseRpc = mock(Class.forName("com.google.android.gms.cloudmessaging.Rpc"));
98
+ ReflectionHelpers.setField(service, "rpc", firebaseRpc);
99
+ }
100
+
101
+ @After
102
+ public void tearDown() {
103
+ ActionHelper.getInstance().clearRegisteredReceivers();
104
+ notifications.cancelAll();
105
+ }
106
+
107
+ @Test
108
+ public void coreDiscoversSourceAdapterOnceAndRebindsTheCurrentToken() {
109
+ FirebaseMessaging provider = mock(FirebaseMessaging.class);
110
+ when(provider.getToken()).thenReturn(Tasks.forResult("current_secret_token"));
111
+ GoogleApiAvailabilityLight availability = mock(GoogleApiAvailabilityLight.class);
112
+ when(availability.isGooglePlayServicesAvailable(context)).thenReturn(0);
113
+ ActionHelper dispatcher = mock(ActionHelper.class);
114
+ JCoreTcpEngine engine = synchronousEngine();
115
+ try (MockedStatic<FirebaseMessaging> firebase = mockStatic(FirebaseMessaging.class);
116
+ MockedStatic<FirebaseApp> app = mockStatic(FirebaseApp.class);
117
+ MockedStatic<GoogleApiAvailabilityLight> google = mockStatic(GoogleApiAvailabilityLight.class);
118
+ MockedStatic<ActionHelper> helper = mockStatic(ActionHelper.class);
119
+ MockedStatic<JCoreTcpEngine> tcp = mockStatic(JCoreTcpEngine.class)) {
120
+ firebase.when(FirebaseMessaging::getInstance).thenReturn(provider);
121
+ google.when(GoogleApiAvailabilityLight::getInstance).thenReturn(availability);
122
+ helper.when(ActionHelper::getInstance).thenReturn(dispatcher);
123
+ tcp.when(JCoreTcpEngine::getInstance).thenReturn(engine);
124
+ JThirdPlatFormInterface.initializeFcm(context);
125
+ JThirdPlatFormInterface.initializeFcm(context);
126
+ Shadows.shadowOf(Looper.getMainLooper()).idle();
127
+ app.verify(() -> FirebaseApp.initializeApp(context), times(2));
128
+ verify(provider, times(2)).getToken();
129
+ ArgumentCaptor<Bundle> actions = ArgumentCaptor.forClass(Bundle.class);
130
+ verify(dispatcher, times(2)).doAction(eq(context), eq("third_action"), actions.capture());
131
+ for (Bundle action : actions.getAllValues()) {
132
+ assertEquals("current_secret_token", action.getString("token"));
133
+ assertEquals((byte) 8, action.getByte("platform"));
134
+ }
135
+ assertLogsExclude("current_secret_token");
136
+ }
137
+ }
138
+
139
+ @Test
140
+ public void missingFirebaseConfigurationDoesNotEscapeCoreInitialization() {
141
+ GoogleApiAvailabilityLight availability = mock(GoogleApiAvailabilityLight.class);
142
+ when(availability.isGooglePlayServicesAvailable(context)).thenReturn(0);
143
+ JCoreTcpEngine engine = synchronousEngine();
144
+ try (MockedStatic<FirebaseMessaging> firebase = mockStatic(FirebaseMessaging.class);
145
+ MockedStatic<FirebaseApp> app = mockStatic(FirebaseApp.class);
146
+ MockedStatic<GoogleApiAvailabilityLight> google = mockStatic(GoogleApiAvailabilityLight.class);
147
+ MockedStatic<JCoreTcpEngine> tcp = mockStatic(JCoreTcpEngine.class)) {
148
+ firebase.when(FirebaseMessaging::getInstance).thenThrow(new IllegalStateException("private_configuration_details"));
149
+ google.when(GoogleApiAvailabilityLight::getInstance).thenReturn(availability);
150
+ tcp.when(JCoreTcpEngine::getInstance).thenReturn(engine);
151
+ JThirdPlatFormInterface.initializeFcm(context);
152
+ assertTrue(JLogger.getRecentLogs().toString().contains("FCM registration unavailable"));
153
+ assertLogsExclude("private_configuration_details");
154
+ }
155
+ }
156
+
157
+ @Test
158
+ public void authorizationGrantedAfterSkippedInitRequestsTokenWithoutReconnect() {
159
+ JCoreStorage.setCollectionAuth(context, false);
160
+ JCoreStorage.setConnected(context, true);
161
+ FirebaseMessaging provider = mock(FirebaseMessaging.class);
162
+ when(provider.getToken()).thenReturn(Tasks.forResult("consented_secret_token"));
163
+ GoogleApiAvailabilityLight availability = mock(GoogleApiAvailabilityLight.class);
164
+ when(availability.isGooglePlayServicesAvailable(context)).thenReturn(0);
165
+ ActionHelper dispatcher = mock(ActionHelper.class);
166
+ JCoreTcpEngine engine = synchronousEngine();
167
+ try (MockedStatic<FirebaseMessaging> firebase = mockStatic(FirebaseMessaging.class);
168
+ MockedStatic<FirebaseApp> app = mockStatic(FirebaseApp.class);
169
+ MockedStatic<GoogleApiAvailabilityLight> google = mockStatic(GoogleApiAvailabilityLight.class);
170
+ MockedStatic<ActionHelper> helper = mockStatic(ActionHelper.class);
171
+ MockedStatic<JCoreTcpEngine> tcp = mockStatic(JCoreTcpEngine.class);
172
+ MockedStatic<JCoreManager> core = mockStatic(JCoreManager.class)) {
173
+ firebase.when(FirebaseMessaging::getInstance).thenReturn(provider);
174
+ google.when(GoogleApiAvailabilityLight::getInstance).thenReturn(availability);
175
+ helper.when(ActionHelper::getInstance).thenReturn(dispatcher);
176
+ tcp.when(JCoreTcpEngine::getInstance).thenReturn(engine);
177
+ JPushInterface.init(context);
178
+ verifyNoInteractions(provider);
179
+ JCollectionAuth.setAuth(context, true);
180
+ JCollectionAuth.setAuth(context, true);
181
+ Shadows.shadowOf(Looper.getMainLooper()).idle();
182
+ verify(provider).getToken();
183
+ assertEquals("consented_secret_token", tokenAction(dispatcher).getString("token"));
184
+ assertTrue(JCoreStorage.isConnected(context));
185
+ core.verify(() -> JCoreManager.init(context));
186
+ assertLogsExclude("consented_secret_token");
187
+ }
188
+ }
189
+
190
+ @Test
191
+ public void grantingAuthorizationWhileStoppedDoesNotRequestRegistration() {
192
+ JCoreStorage.setCollectionAuth(context, false);
193
+ JCoreStorage.setPushStopped(context, true);
194
+ try (MockedStatic<JCoreTcpEngine> tcp = mockStatic(JCoreTcpEngine.class)) {
195
+ JCollectionAuth.setAuth(context, true);
196
+ assertTrue(JCollectionAuth.isAuth(context));
197
+ tcp.verifyNoInteractions();
198
+ }
199
+ }
200
+
201
+ @Test
202
+ public void revokedAuthorizationCancelsQueuedFcmRegistration() {
203
+ JCoreStorage.setCollectionAuth(context, false);
204
+ JCoreTcpEngine engine = mock(JCoreTcpEngine.class);
205
+ try (MockedStatic<JCoreTcpEngine> tcp = mockStatic(JCoreTcpEngine.class);
206
+ MockedStatic<FirebaseMessaging> firebase = mockStatic(FirebaseMessaging.class)) {
207
+ tcp.when(JCoreTcpEngine::getInstance).thenReturn(engine);
208
+ JCollectionAuth.setAuth(context, true);
209
+ ArgumentCaptor<Runnable> work = ArgumentCaptor.forClass(Runnable.class);
210
+ verify(engine).executeAsync(work.capture());
211
+ JCollectionAuth.setAuth(context, false);
212
+ work.getValue().run();
213
+ firebase.verifyNoInteractions();
214
+ assertFalse(JCollectionAuth.isAuth(context));
215
+ }
216
+ }
217
+
218
+ @Test
219
+ public void tokenSuccessDispatchesPlatformEightWithoutPersistingBeforeAck() {
220
+ JCoreStorage.saveThirdToken(context, (byte) 8, "previous_acknowledged_token");
221
+ FirebaseMessaging provider = mock(FirebaseMessaging.class);
222
+ TaskCompletionSource<String> request = new TaskCompletionSource<>();
223
+ when(provider.getToken()).thenReturn(request.getTask());
224
+ ActionHelper dispatcher = mock(ActionHelper.class);
225
+ try (MockedStatic<FirebaseMessaging> firebase = mockStatic(FirebaseMessaging.class);
226
+ MockedStatic<ActionHelper> helper = mockStatic(ActionHelper.class)) {
227
+ firebase.when(FirebaseMessaging::getInstance).thenReturn(provider);
228
+ helper.when(ActionHelper::getInstance).thenReturn(dispatcher);
229
+ assertEquals("previous_acknowledged_token", new FcmPushManager().getToken(context));
230
+ verifyNoInteractions(dispatcher);
231
+ request.setResult("rotated_secret_token");
232
+ Shadows.shadowOf(Looper.getMainLooper()).idle();
233
+ Bundle token = tokenAction(dispatcher);
234
+ assertEquals("rotated_secret_token", token.getString("token"));
235
+ assertEquals((byte) 8, token.getByte("platform"));
236
+ assertEquals("previous_acknowledged_token", JCoreStorage.getThirdToken(context, (byte) 8));
237
+ assertLogsExclude("rotated_secret_token");
238
+ }
239
+ }
240
+
241
+ @Test
242
+ public void failedTokenRequestDoesNotBindOrLogRawError() {
243
+ FirebaseMessaging provider = mock(FirebaseMessaging.class);
244
+ when(provider.getToken()).thenReturn(Tasks.forException(new IOException("secret_provider_error")));
245
+ ActionHelper dispatcher = mock(ActionHelper.class);
246
+ try (MockedStatic<FirebaseMessaging> firebase = mockStatic(FirebaseMessaging.class);
247
+ MockedStatic<ActionHelper> helper = mockStatic(ActionHelper.class)) {
248
+ firebase.when(FirebaseMessaging::getInstance).thenReturn(provider);
249
+ helper.when(ActionHelper::getInstance).thenReturn(dispatcher);
250
+ new FcmPushManager().getToken(context);
251
+ Shadows.shadowOf(Looper.getMainLooper()).idle();
252
+ verifyNoInteractions(dispatcher);
253
+ assertTrue(JLogger.getRecentLogs().toString().contains("IOException"));
254
+ assertLogsExclude("secret_provider_error");
255
+ }
256
+ }
257
+
258
+ @Test
259
+ public void tokenCallbackRechecksStopAndAuthorizationAfterRequest() {
260
+ for (boolean stopped : new boolean[]{true, false}) {
261
+ JCoreStorage.setPushStopped(context, false);
262
+ JCoreStorage.setCollectionAuth(context, true);
263
+ FirebaseMessaging provider = mock(FirebaseMessaging.class);
264
+ TaskCompletionSource<String> request = new TaskCompletionSource<>();
265
+ when(provider.getToken()).thenReturn(request.getTask());
266
+ ActionHelper dispatcher = mock(ActionHelper.class);
267
+ try (MockedStatic<FirebaseMessaging> firebase = mockStatic(FirebaseMessaging.class);
268
+ MockedStatic<ActionHelper> helper = mockStatic(ActionHelper.class)) {
269
+ firebase.when(FirebaseMessaging::getInstance).thenReturn(provider);
270
+ helper.when(ActionHelper::getInstance).thenReturn(dispatcher);
271
+ new FcmPushManager().getToken(context);
272
+ JCoreStorage.setPushStopped(context, stopped);
273
+ JCoreStorage.setCollectionAuth(context, stopped);
274
+ request.setResult("late_secret_token");
275
+ Shadows.shadowOf(Looper.getMainLooper()).idle();
276
+ verifyNoInteractions(dispatcher);
277
+ }
278
+ }
279
+ }
280
+
281
+ @Test
282
+ public void newTokenInitializesCoreAndForwardsRotation() {
283
+ JCoreStorage.setConnected(context, true);
284
+ FirebaseMessaging provider = mock(FirebaseMessaging.class);
285
+ when(provider.getToken()).thenReturn(Tasks.forResult("new_secret_token"));
286
+ GoogleApiAvailabilityLight availability = mock(GoogleApiAvailabilityLight.class);
287
+ when(availability.isGooglePlayServicesAvailable(context)).thenReturn(0);
288
+ JCoreTcpEngine engine = synchronousEngine();
289
+ try (MockedStatic<JCoreManager> core = mockStatic(JCoreManager.class);
290
+ MockedStatic<FirebaseMessaging> firebase = mockStatic(FirebaseMessaging.class);
291
+ MockedStatic<FirebaseApp> app = mockStatic(FirebaseApp.class);
292
+ MockedStatic<GoogleApiAvailabilityLight> google = mockStatic(GoogleApiAvailabilityLight.class);
293
+ MockedStatic<JCoreTcpEngine> tcp = mockStatic(JCoreTcpEngine.class)) {
294
+ firebase.when(FirebaseMessaging::getInstance).thenReturn(provider);
295
+ google.when(GoogleApiAvailabilityLight::getInstance).thenReturn(availability);
296
+ tcp.when(JCoreTcpEngine::getInstance).thenReturn(engine);
297
+ service.handleIntent(new Intent("com.google.firebase.messaging.NEW_TOKEN").putExtra("token", "new_secret_token"));
298
+ Shadows.shadowOf(Looper.getMainLooper()).idle();
299
+ core.verify(() -> JCoreManager.init(context));
300
+ ArgumentCaptor<byte[]> body = ArgumentCaptor.forClass(byte[].class);
301
+ verify(engine).enqueueOrSendRequest(eq(context), eq(JCoreWireCodec.CMD_THIRD_TOKEN),
302
+ eq(JCoreWireCodec.VER_THIRD_TOKEN), anyLong(), body.capture(), anyLong(), eq(3),
303
+ any(JCoreTcpEngine.RequestCallback.class));
304
+ assertArrayEquals(JCoreWireCodec.buildThirdTokenPayload("new_secret_token", (byte) 8, false), body.getValue());
305
+ verifyNoInteractions(provider);
306
+ assertLogsExclude("new_secret_token");
307
+ }
308
+ }
309
+
310
+ @Test
311
+ public void stoppedOrUnauthorizedNewTokenDoesNotInitializeCore() {
312
+ try (MockedStatic<JCoreManager> core = mockStatic(JCoreManager.class)) {
313
+ JCoreStorage.setPushStopped(context, true);
314
+ service.onNewToken("ignored");
315
+ JCoreStorage.setPushStopped(context, false);
316
+ JCoreStorage.setCollectionAuth(context, false);
317
+ service.onNewToken("ignored");
318
+ core.verifyNoInteractions();
319
+ }
320
+ }
321
+
322
+ @Test
323
+ public void emptyTokensAreIgnored() {
324
+ ActionHelper dispatcher = mock(ActionHelper.class);
325
+ try (MockedStatic<ActionHelper> helper = mockStatic(ActionHelper.class)) {
326
+ helper.when(ActionHelper::getInstance).thenReturn(dispatcher);
327
+ FcmPushManager.acceptToken(context, null);
328
+ FcmPushManager.acceptToken(context, "");
329
+ verifyNoInteractions(dispatcher);
330
+ }
331
+ }
332
+
333
+ @Test
334
+ public void googlePlayAvailabilityAndNotificationDelegation() {
335
+ GoogleApiAvailabilityLight availability = mock(GoogleApiAvailabilityLight.class);
336
+ when(availability.isGooglePlayServicesAvailable(context)).thenReturn(0, 9);
337
+ try (MockedStatic<GoogleApiAvailabilityLight> google = mockStatic(GoogleApiAvailabilityLight.class);
338
+ MockedStatic<JPushInterface> core = mockStatic(JPushInterface.class)) {
339
+ google.when(GoogleApiAvailabilityLight::getInstance).thenReturn(availability);
340
+ FcmPushManager manager = new FcmPushManager();
341
+ assertTrue(manager.isSupport(context));
342
+ assertFalse(manager.isSupport(context));
343
+ assertFalse(manager.isSupport(null));
344
+ manager.clearNotification(context, 123);
345
+ manager.clearAllNotification(context);
346
+ core.verify(() -> JPushInterface.clearNotificationById(context, 123));
347
+ core.verify(() -> JPushInterface.clearAllNotifications(context));
348
+ }
349
+ }
350
+
351
+ @Test
352
+ public void firebaseInitializationAndApplicationMetadata() {
353
+ FirebaseApp app = mock(FirebaseApp.class);
354
+ FirebaseOptions options = new FirebaseOptions.Builder()
355
+ .setApplicationId("1:123456:android:synthetic").setApiKey("synthetic_api_key").build();
356
+ when(app.getOptions()).thenReturn(options);
357
+ try (MockedStatic<FirebaseApp> firebase = mockStatic(FirebaseApp.class)) {
358
+ firebase.when(FirebaseApp::getInstance).thenReturn(app);
359
+ FcmPushManager manager = new FcmPushManager();
360
+ manager.init(context);
361
+ manager.register(context, new Bundle());
362
+ firebase.verify(() -> FirebaseApp.initializeApp(context), times(2));
363
+ assertEquals(options.getApplicationId(), manager.getAppId(context));
364
+ assertEquals(options.getApiKey(), manager.getAppkey(context));
365
+ assertEquals((byte) 8, manager.getRomType(context));
366
+ assertFalse(manager.isNeedClearToken(context));
367
+ }
368
+ }
369
+
370
+ @Test
371
+ public void firebaseNotificationFieldsFillMetadataAndTapOpensOnce() {
372
+ Bundle payload = metadata("{\"_jmsgid_\":\"fcm_notification\",\"n_extras\":{\"route\":\"synthetic_route\"}}");
373
+ payload.putString("gcm.n.e", "1");
374
+ payload.putString("gcm.n.title", "private_title");
375
+ payload.putString("gcm.n.body", "private_body");
376
+ service.onMessageReceived(new RemoteMessage(payload));
377
+ assertEquals(1, arrived.size());
378
+ assertEquals("private_title", arrived.get(0).notificationTitle);
379
+ assertEquals("private_body", arrived.get(0).notificationContent);
380
+ assertEquals(8, arrived.get(0).platform);
381
+ List<Notification> posted = Shadows.shadowOf(notifications).getAllNotifications();
382
+ assertEquals(1, posted.size());
383
+ assertEquals("private_title", posted.get(0).extras.getString(Notification.EXTRA_TITLE));
384
+ Intent tap = Shadows.shadowOf(posted.get(0).contentIntent).getSavedIntent();
385
+ assertEquals(JNotifyActivity.class.getName(), tap.getComponent().getClassName());
386
+ Robolectric.buildActivity(JNotifyActivity.class, tap).create();
387
+ assertEquals(1, opened.size());
388
+ assertEquals("fcm_notification", opened.get(0).msgId);
389
+ assertTrue(opened.get(0).notificationExtras.contains("synthetic_route"));
390
+ assertLogsExclude("private_title", "private_body", "synthetic_route");
391
+ }
392
+
393
+ @Test
394
+ public void postedDataOnlyTapIsIgnoredAfterStopOrAuthorizationWithdrawal() {
395
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE);
396
+ ActivityInfo host = new ActivityInfo();
397
+ host.packageName = context.getPackageName();
398
+ host.name = context.getPackageName() + ".MainActivity";
399
+ host.applicationInfo = new ApplicationInfo(context.getApplicationInfo());
400
+ ResolveInfo resolvedHost = new ResolveInfo();
401
+ resolvedHost.activityInfo = host;
402
+ Intent launcher = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER).setPackage(context.getPackageName());
403
+ Shadows.shadowOf(context.getPackageManager()).addResolveInfoForIntent(launcher, resolvedHost);
404
+ for (boolean stopped : new boolean[]{true, false}) {
405
+ JCoreStorage.setPushStopped(context, false);
406
+ JCoreStorage.setCollectionAuth(context, true);
407
+ service.handleIntent(firebaseReceiveIntent("deferred_tap_" + stopped, false));
408
+ List<Notification> posted = Shadows.shadowOf(notifications).getAllNotifications();
409
+ assertEquals(1, posted.size());
410
+ Intent tap = Shadows.shadowOf(posted.get(0).contentIntent).getSavedIntent();
411
+ JCoreStorage.setPushStopped(context, stopped);
412
+ JCoreStorage.setCollectionAuth(context, stopped);
413
+ Robolectric.buildActivity(JNotifyActivity.class, tap).create();
414
+ assertTrue(opened.isEmpty());
415
+ assertNull(Shadows.shadowOf(RuntimeEnvironment.getApplication()).getNextStartedActivity());
416
+ notifications.cancelAll();
417
+ }
418
+ }
419
+
420
+ @Test
421
+ public void compressedMetadataRetainsItsOwnTitleAndExtras() {
422
+ Bundle payload = metadata(compress("{\"msg_id\":\"zipped\",\"override_msg_id\":\"\",\"n_title\":\"metadata_title\",\"n_content\":\"metadata_body\",\"n_extras\":{\"route\":\"private_route\"}}"));
423
+ payload.putString("gcm.n.e", "1");
424
+ payload.putString("gcm.n.title", "fallback_title");
425
+ payload.putString("gcm.n.body", "fallback_body");
426
+ service.onMessageReceived(new RemoteMessage(payload));
427
+ assertEquals(1, arrived.size());
428
+ assertEquals("zipped", arrived.get(0).msgId);
429
+ assertEquals("metadata_title", arrived.get(0).notificationTitle);
430
+ assertTrue(arrived.get(0).notificationExtras.contains("private_route"));
431
+ }
432
+
433
+ @Test
434
+ public void standardFirebaseDataUsesTransportIdAndPreservesExtras() {
435
+ Bundle payload = new Bundle();
436
+ payload.putString("google.message_id", "transport_id");
437
+ payload.putString("n_title", "Synthetic notification");
438
+ payload.putString("n_content", "Synthetic body");
439
+ payload.putString("route", "synthetic_route");
440
+ service.onMessageReceived(new RemoteMessage(payload));
441
+ assertEquals(1, arrived.size());
442
+ assertEquals("transport_id", arrived.get(0).msgId);
443
+ assertTrue(arrived.get(0).notificationExtras.contains("synthetic_route"));
444
+ }
445
+
446
+ @Test
447
+ public void customDataDoesNotDisplayANotification() {
448
+ service.onMessageReceived(new RemoteMessage(metadata("{\"msg_id\":\"custom_id\",\"message\":\"private_custom_body\",\"content_type\":\"text\",\"extras\":{\"route\":\"private_custom_route\"}}")));
449
+ assertEquals(1, custom.size());
450
+ assertEquals("custom_id", custom.get(0).messageId);
451
+ assertEquals("private_custom_body", custom.get(0).message);
452
+ assertEquals("text", custom.get(0).contentType);
453
+ assertTrue(arrived.isEmpty());
454
+ assertEquals(0, Shadows.shadowOf(notifications).getAllNotifications().size());
455
+ assertLogsExclude("private_custom_body", "private_custom_route");
456
+ }
457
+
458
+ @Test
459
+ public void malformedAndOversizedPayloadsDoNotReachCallbacks() {
460
+ service.onMessageReceived(new RemoteMessage(metadata("{broken")));
461
+ service.onMessageReceived(new RemoteMessage(metadata("{\"jg_c\":\"not-gzip\"}")));
462
+ StringBuilder oversized = new StringBuilder();
463
+ for (int i = 0; i < 70000; i++) oversized.append('x');
464
+ service.onMessageReceived(new RemoteMessage(metadata(compress(oversized.toString()))));
465
+ assertTrue(arrived.isEmpty());
466
+ assertTrue(custom.isEmpty());
467
+ assertEquals(0, Shadows.shadowOf(notifications).getAllNotifications().size());
468
+ }
469
+
470
+ @Test
471
+ public void stoppedAndUnauthorizedNotificationsAreIgnored() {
472
+ String body = "{\"msg_id\":\"blocked\",\"n_content\":\"synthetic\"}";
473
+ JCoreStorage.setPushStopped(context, true);
474
+ service.onMessageReceived(new RemoteMessage(metadata(body)));
475
+ JCoreStorage.setPushStopped(context, false);
476
+ JCoreStorage.setCollectionAuth(context, false);
477
+ service.onMessageReceived(new RemoteMessage(metadata(body)));
478
+ JCoreStorage.setCollectionAuth(context, true);
479
+ assertTrue(arrived.isEmpty());
480
+ assertEquals(0, Shadows.shadowOf(notifications).getAllNotifications().size());
481
+ }
482
+
483
+ @Test
484
+ public void foregroundDisplayDisabledSuppressesNotificationAndArrival() {
485
+ service.onMessageReceived(foregroundSuppressedMessage());
486
+ assertTrue(arrived.isEmpty());
487
+ assertEquals(0, Shadows.shadowOf(notifications).getAllNotifications().size());
488
+ }
489
+
490
+ @Test
491
+ public void backgroundDataOnlyMessageStillDisplaysAndDispatchesArrival() {
492
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE);
493
+ RemoteMessage message = foregroundSuppressedMessage();
494
+ assertNull(message.getNotification());
495
+ service.onMessageReceived(message);
496
+ assertEquals(1, arrived.size());
497
+ assertEquals("foreground_suppressed", arrived.get(0).msgId);
498
+ assertEquals(1, Shadows.shadowOf(notifications).getAllNotifications().size());
499
+ }
500
+
501
+ @Test
502
+ public void foregroundDisplayDecisionUsesCurrentProcessStateForEachMessage() {
503
+ service.onMessageReceived(foregroundSuppressedMessage());
504
+ assertTrue(arrived.isEmpty());
505
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE);
506
+ service.onMessageReceived(foregroundSuppressedMessage());
507
+ assertEquals(1, arrived.size());
508
+ notifications.cancelAll();
509
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND);
510
+ service.onMessageReceived(foregroundSuppressedMessage());
511
+ assertEquals(1, arrived.size());
512
+ assertEquals(0, Shadows.shadowOf(notifications).getAllNotifications().size());
513
+ }
514
+
515
+ @Test
516
+ public void lockedScreenDoesNotSuppressDataOnlyNotification() {
517
+ KeyguardManager keyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
518
+ Shadows.shadowOf(keyguard).setKeyguardLocked(true);
519
+ service.onMessageReceived(foregroundSuppressedMessage());
520
+ assertEquals(1, arrived.size());
521
+ assertEquals(1, Shadows.shadowOf(notifications).getAllNotifications().size());
522
+ }
523
+
524
+ @Test
525
+ public void stoppedBackgroundCombinedIntentCannotUseFirebaseAutomaticDisplay() {
526
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE);
527
+ JCoreStorage.setPushStopped(context, true);
528
+ service.handleIntent(firebaseReceiveIntent("stopped_combined", true));
529
+ assertTrue(arrived.isEmpty());
530
+ assertEquals(0, Shadows.shadowOf(notifications).getAllNotifications().size());
531
+ verifyNoInteractions(firebaseRpc);
532
+ assertLogsExclude("private_title", "private_body");
533
+ }
534
+
535
+ @Test
536
+ public void unauthorizedBackgroundNotificationIntentCannotUseFirebaseAutomaticDisplay() {
537
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE);
538
+ JCoreStorage.setCollectionAuth(context, false);
539
+ Intent notificationOnly = firebaseReceiveIntent("unauthorized_notification", true);
540
+ notificationOnly.removeExtra("JMessageExtra");
541
+ service.handleIntent(notificationOnly);
542
+ assertTrue(arrived.isEmpty());
543
+ assertEquals(0, Shadows.shadowOf(notifications).getAllNotifications().size());
544
+ verifyNoInteractions(firebaseRpc);
545
+ assertLogsExclude("private_title", "private_body");
546
+ }
547
+
548
+ @Test
549
+ public void authorizedBackgroundCombinedIntentKeepsFirebaseDisplayAndDeduplication() {
550
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE);
551
+ Intent receive = firebaseReceiveIntent("allowed_combined", true);
552
+ service.handleIntent(receive);
553
+ service.handleIntent(new Intent(receive));
554
+ assertEquals(1, Shadows.shadowOf(notifications).getAllNotifications().size());
555
+ assertTrue(arrived.isEmpty());
556
+ assertEquals(2, mockingDetails(firebaseRpc).getInvocations().size());
557
+ }
558
+
559
+ @Test
560
+ public void authorizedBackgroundDataOnlyIntentUsesNativeDisplayAndFirebaseDeduplication() {
561
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE);
562
+ Intent receive = firebaseReceiveIntent("allowed_data", false);
563
+ service.handleIntent(receive);
564
+ service.handleIntent(new Intent(receive));
565
+ assertEquals(1, arrived.size());
566
+ assertEquals("allowed_data", arrived.get(0).msgId);
567
+ assertEquals(1, Shadows.shadowOf(notifications).getAllNotifications().size());
568
+ }
569
+
570
+ @Test
571
+ public void receiveGateRechecksStateAfterStopWithdrawalAndRegrant() {
572
+ setProcessImportance(ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE);
573
+ JCoreStorage.setPushStopped(context, true);
574
+ service.handleIntent(firebaseReceiveIntent("stopped", true));
575
+ JCoreStorage.setPushStopped(context, false);
576
+ JCoreStorage.setCollectionAuth(context, false);
577
+ service.handleIntent(firebaseReceiveIntent("withdrawn", true));
578
+ assertEquals(0, Shadows.shadowOf(notifications).getAllNotifications().size());
579
+ JCoreStorage.setCollectionAuth(context, true);
580
+ service.handleIntent(firebaseReceiveIntent("regranted", true));
581
+ assertEquals(1, Shadows.shadowOf(notifications).getAllNotifications().size());
582
+ assertEquals(1, mockingDetails(firebaseRpc).getInvocations().size());
583
+ }
584
+
585
+ @Test
586
+ @Config(sdk = 34)
587
+ public void startupRevokesOldPlayServicesDelegateWithoutTokenOrConsent() {
588
+ JCoreStorage.setPushStopped(context, true);
589
+ JCoreStorage.setCollectionAuth(context, false);
590
+ notifications.setNotificationDelegate("com.google.android.gms");
591
+ try (MockedStatic<FirebaseMessaging> firebase = mockStatic(FirebaseMessaging.class)) {
592
+ Robolectric.buildContentProvider(FcmInitProvider.class).create();
593
+ assertNull(notifications.getNotificationDelegate());
594
+ assertFalse(JCollectionAuth.isAuth(context));
595
+ assertTrue(JCoreStorage.isPushStopped(context));
596
+ firebase.verifyNoInteractions();
597
+ }
598
+ }
599
+
600
+ @Test
601
+ @Config(sdk = 34)
602
+ public void startupPreservesOtherNotificationDelegates() {
603
+ notifications.setNotificationDelegate("other.notification.delegate");
604
+ Robolectric.buildContentProvider(FcmInitProvider.class).create();
605
+ assertEquals("other.notification.delegate", notifications.getNotificationDelegate());
606
+ }
607
+
608
+ @Test
609
+ public void automaticFirebaseTapLaunchesHostAndKeepsCompressedMetadata() {
610
+ ActivityInfo activity = new ActivityInfo();
611
+ activity.packageName = context.getPackageName();
612
+ activity.name = context.getPackageName() + ".MainActivity";
613
+ activity.applicationInfo = new ApplicationInfo(context.getApplicationInfo());
614
+ ResolveInfo resolve = new ResolveInfo();
615
+ resolve.activityInfo = activity;
616
+ Intent launcher = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER).setPackage(context.getPackageName());
617
+ Shadows.shadowOf(context.getPackageManager()).addResolveInfoForIntent(launcher, resolve);
618
+ Intent tap = new Intent(JPushInterface.ACTION_NOTIFICATION_OPENED)
619
+ .putExtra("JMessageExtra", compress("{\"msg_id\":\"cold_tap\",\"n_extras\":{\"route\":\"synthetic_route\"}}"));
620
+ Robolectric.buildActivity(JNotifyActivity.class, tap).create();
621
+ assertEquals(1, opened.size());
622
+ assertEquals("cold_tap", opened.get(0).msgId);
623
+ Intent launched = Shadows.shadowOf(RuntimeEnvironment.getApplication()).getNextStartedActivity();
624
+ assertNotNull(launched);
625
+ assertEquals(activity.name, launched.getComponent().getClassName());
626
+ assertTrue(opened.get(0).notificationExtras.contains("synthetic_route"));
627
+ }
628
+
629
+ private static Bundle metadata(String json) {
630
+ Bundle payload = new Bundle();
631
+ payload.putString("JMessageExtra", json);
632
+ return payload;
633
+ }
634
+
635
+ private static RemoteMessage foregroundSuppressedMessage() {
636
+ return new RemoteMessage(metadata("{\"msg_id\":\"foreground_suppressed\",\"n_content\":\"synthetic\",\"n_foreground\":\"0\"}"));
637
+ }
638
+
639
+ private static Intent firebaseReceiveIntent(String id, boolean notification) {
640
+ Intent receive = new Intent("com.google.android.c2dm.intent.RECEIVE")
641
+ .putExtra("google.message_id", id)
642
+ .putExtras(metadata("{\"msg_id\":\"" + id + "\",\"n_content\":\"private_body\",\"n_foreground\":\"0\"}"));
643
+ if (notification) {
644
+ receive.putExtra("gcm.n.e", "1");
645
+ receive.putExtra("gcm.n.title", "private_title");
646
+ receive.putExtra("gcm.n.body", "private_body");
647
+ receive.putExtra("gcm.n.icon", "android");
648
+ }
649
+ return receive;
650
+ }
651
+
652
+ private void setProcessImportance(int importance) {
653
+ ActivityManager.RunningAppProcessInfo process = new ActivityManager.RunningAppProcessInfo();
654
+ process.pid = Process.myPid();
655
+ process.importance = importance;
656
+ ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
657
+ Shadows.shadowOf(manager).setProcesses(Collections.singletonList(process));
658
+ }
659
+
660
+ private static String compress(String json) {
661
+ return "{\"jg_c\":\"" + JCoreCrypto.base64Encode(JCoreCrypto.gzipCompress(json.getBytes(StandardCharsets.UTF_8))) + "\"}";
662
+ }
663
+
664
+ private Bundle tokenAction(ActionHelper dispatcher) {
665
+ ArgumentCaptor<Bundle> action = ArgumentCaptor.forClass(Bundle.class);
666
+ verify(dispatcher).doAction(eq(context), eq("third_action"), action.capture());
667
+ assertEquals(JThirdPlatFormInterface.ACTION_REGISTER_TOKEN, action.getValue().getString("third_key_action"));
668
+ return action.getValue();
669
+ }
670
+
671
+ private static JCoreTcpEngine synchronousEngine() {
672
+ JCoreTcpEngine engine = mock(JCoreTcpEngine.class);
673
+ doAnswer(call -> {
674
+ call.<Runnable>getArgument(0).run();
675
+ return null;
676
+ }).when(engine).executeAsync(any(Runnable.class));
677
+ return engine;
678
+ }
679
+
680
+ private static void assertLogsExclude(String... values) {
681
+ String logs = JLogger.getRecentLogs().toString();
682
+ for (String value : values) assertFalse("Sensitive payload appeared in native logger", logs.contains(value));
683
+ }
684
+ }
@@ -0,0 +1,54 @@
1
+ package com.onekey.jpush.fcm;
2
+
3
+ import static org.junit.Assert.*;
4
+
5
+ import android.content.Context;
6
+ import android.content.pm.PackageInfo;
7
+ import android.content.pm.PackageManager;
8
+ import android.content.pm.ProviderInfo;
9
+ import android.content.pm.ServiceInfo;
10
+ import android.content.Intent;
11
+ import org.junit.Test;
12
+ import org.junit.runner.RunWith;
13
+ import org.robolectric.RobolectricTestRunner;
14
+ import org.robolectric.RuntimeEnvironment;
15
+ import org.robolectric.annotation.Config;
16
+
17
+ @RunWith(RobolectricTestRunner.class)
18
+ @Config(sdk = 34)
19
+ public class FcmManifestTest {
20
+ @Test
21
+ public void mergedManifestRoutesOnlyToGuardedServiceAndDisablesDelegation() throws Exception {
22
+ Context context = RuntimeEnvironment.getApplication();
23
+ PackageManager packages = context.getPackageManager();
24
+ PackageInfo info = packages.getPackageInfo(context.getPackageName(),
25
+ PackageManager.GET_META_DATA | PackageManager.GET_PROVIDERS | PackageManager.GET_SERVICES);
26
+ assertTrue(info.applicationInfo.metaData.containsKey("firebase_messaging_notification_delegation_enabled"));
27
+ assertFalse(info.applicationInfo.metaData.getBoolean("firebase_messaging_notification_delegation_enabled"));
28
+ assertTrue(info.applicationInfo.metaData.containsKey("firebase_messaging_auto_init_enabled"));
29
+ assertFalse(info.applicationInfo.metaData.getBoolean("firebase_messaging_auto_init_enabled"));
30
+ boolean initializerFound = false;
31
+ for (ProviderInfo provider : info.providers) {
32
+ if (FcmInitProvider.class.getName().equals(provider.name)) {
33
+ initializerFound = true;
34
+ assertFalse(provider.exported);
35
+ }
36
+ }
37
+ assertTrue("The startup delegate cleanup must be installed", initializerFound);
38
+ boolean guardedServiceFound = false;
39
+ for (ServiceInfo service : info.services) {
40
+ assertNotEquals("Unguarded Firebase fallback must not be installed",
41
+ "com.google.firebase.messaging.FirebaseMessagingService", service.name);
42
+ if (FcmMessagingService.class.getName().equals(service.name)) {
43
+ guardedServiceFound = true;
44
+ assertFalse(service.exported);
45
+ assertFalse(service.directBootAware);
46
+ }
47
+ }
48
+ assertTrue("The guarded receive service must be installed", guardedServiceFound);
49
+ android.content.pm.ResolveInfo handler = packages.resolveService(
50
+ new Intent("com.google.firebase.MESSAGING_EVENT").setPackage(context.getPackageName()), 0);
51
+ assertNotNull(handler);
52
+ assertEquals(FcmMessagingService.class.getName(), handler.serviceInfo.name);
53
+ }
54
+ }
@@ -2,10 +2,15 @@ package cn.jiguang.api.utils;
2
2
 
3
3
  import android.content.Context;
4
4
  import cn.jiguang.core.JCoreStorage;
5
+ import cn.jpush.android.api.JThirdPlatFormInterface;
5
6
 
6
7
  public class JCollectionAuth {
7
- public static void setAuth(Context context, boolean auth) {
8
+ public static synchronized void setAuth(Context context, boolean auth) {
9
+ boolean wasAuthorized = JCoreStorage.isCollectionAuth(context);
8
10
  JCoreStorage.setCollectionAuth(context, auth);
11
+ if (auth && !wasAuthorized) {
12
+ JThirdPlatFormInterface.initializeFcm(context);
13
+ }
9
14
  }
10
15
 
11
16
  public static boolean isAuth(Context context) {
@@ -1,6 +1,8 @@
1
1
  package cn.jpush.android.local;
2
2
 
3
3
  import android.app.Activity;
4
+ import android.app.ActivityManager;
5
+ import android.app.KeyguardManager;
4
6
  import android.app.Notification;
5
7
  import android.app.NotificationChannel;
6
8
  import android.app.NotificationManager;
@@ -154,7 +156,7 @@ public class ActionHelper {
154
156
  custom.platform = 8;
155
157
  dispatchCustomMessageEvent(context, custom);
156
158
  } else {
157
- if ("0".equals(message.displayForeground)) {
159
+ if ("0".equals(message.displayForeground) && isApplicationForeground(context)) {
158
160
  return;
159
161
  }
160
162
  postSystemNotification(context, message);
@@ -172,6 +174,16 @@ public class ActionHelper {
172
174
  }
173
175
  }
174
176
 
177
+ private boolean isApplicationForeground(Context context) {
178
+ KeyguardManager keyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
179
+ if (keyguard != null && keyguard.isKeyguardLocked()) {
180
+ return false;
181
+ }
182
+ ActivityManager.RunningAppProcessInfo process = new ActivityManager.RunningAppProcessInfo();
183
+ ActivityManager.getMyMemoryState(process);
184
+ return process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
185
+ }
186
+
175
187
  public void doSingleAction(Context context, String action, Object data) {
176
188
  }
177
189
 
@@ -220,6 +232,10 @@ public class ActionHelper {
220
232
  }
221
233
  if (msg instanceof NotificationMessage) {
222
234
  NotificationMessage notificationMessage = (NotificationMessage) msg;
235
+ if (notificationMessage.platform == 8
236
+ && (JCoreStorage.isPushStopped(context) || !JCoreStorage.isCollectionAuth(context))) {
237
+ return;
238
+ }
223
239
  dispatchNotificationEvent(context, "notify_opened", notificationMessage);
224
240
  broadcastNotificationOpened(context, notificationMessage);
225
241
  if (fcmClick || notificationMessage.platform == 8) {
@@ -0,0 +1,110 @@
1
+ package com.margelo.nitro.reactnativejpush;
2
+
3
+ import static org.junit.Assert.*;
4
+ import cn.jiguang.core.JCoreCrypto;
5
+ import cn.jiguang.core.JCoreWireCodec;
6
+ import java.io.InputStream;
7
+ import java.io.OutputStream;
8
+ import java.net.DatagramPacket;
9
+ import java.net.DatagramSocket;
10
+ import java.net.InetAddress;
11
+ import java.net.InetSocketAddress;
12
+ import java.net.Socket;
13
+ import java.util.Arrays;
14
+ import java.util.Collections;
15
+ import java.util.Set;
16
+ import org.junit.Assume;
17
+ import org.junit.Test;
18
+
19
+ public class JPushCloudHandshakeTest {
20
+ @Test
21
+ public void liveCloudHandshake() throws Exception {
22
+ boolean enabled = "1".equals(System.getenv("JPUSH_LIVE_TESTS"));
23
+ if (!enabled) {
24
+ System.out.println("SKIPPED live Jiguang handshake: set JPUSH_LIVE_TESTS=1 to enable");
25
+ }
26
+ Assume.assumeTrue("External cloud handshake requires JPUSH_LIVE_TESTS=1", enabled);
27
+ String sisJson;
28
+ try (DatagramSocket udp = new DatagramSocket()) {
29
+ String request = JCoreWireCodec.buildSisRequestJson(
30
+ 1, ReactNativeJpushProtocolTest.DEMO_TEST_APP_KEY, 0L, "",
31
+ Collections.<String>emptyList(), 1
32
+ );
33
+ byte[] packet = JCoreWireCodec.encodeUdpSisPacket(request, JCoreCrypto.nextPositiveSeed24());
34
+ udp.setSoTimeout(2500);
35
+ udp.send(new DatagramPacket(packet, packet.length, InetAddress.getByName("s.jpush.cn"), 19000));
36
+ DatagramPacket response = new DatagramPacket(new byte[4096], 4096);
37
+ udp.receive(response);
38
+ sisJson = JCoreWireCodec.decodeUdpSisPacket(Arrays.copyOf(response.getData(), response.getLength()));
39
+ }
40
+ assertTrue("SIS response must contain endpoints", sisJson.contains("\"ips\""));
41
+ Set<String> endpoints = JCoreWireCodec.extractJsonStringArray(sisJson, "ips");
42
+ assertFalse("SIS endpoints must not be empty", endpoints.isEmpty());
43
+ String endpoint = endpoints.iterator().next();
44
+ int colon = endpoint.lastIndexOf(':');
45
+ try (Socket tcp = new Socket()) {
46
+ tcp.connect(new InetSocketAddress(endpoint.substring(0, colon), Integer.parseInt(endpoint.substring(colon + 1))), 3000);
47
+ tcp.setSoTimeout(5000);
48
+ performHandshake(tcp.getInputStream(), tcp.getOutputStream());
49
+ }
50
+ System.out.println("Live Jiguang register/login handshake passed");
51
+ }
52
+
53
+ // Shared by the opt-in cloud test and deterministic failure-propagation regressions.
54
+ static void performHandshake(InputStream in, OutputStream out) throws Exception {
55
+ int seed = JCoreCrypto.nextRegisterSeed();
56
+ JCoreWireCodec.setLastRegisterSeed(seed);
57
+ byte[] regBody = JCoreWireCodec.buildRegisterPayload(
58
+ ReactNativeJpushProtocolTest.DEMO_TEST_APP_KEY,
59
+ "developer-default",
60
+ ReactNativeJpushProtocolTest.DEMO_TEST_PACKAGE_NAME,
61
+ "msgbench_auto_001"
62
+ );
63
+ byte[] regFrame = JCoreWireCodec.encodeFrame(
64
+ JCoreWireCodec.CMD_REGISTER,
65
+ JCoreWireCodec.VER_REGISTER,
66
+ 101L,
67
+ seed,
68
+ 0L,
69
+ regBody
70
+ );
71
+ out.write(regFrame);
72
+ out.flush();
73
+
74
+ JCoreWireCodec.DecodedFrame regRespFrame = JCoreWireCodec.readFrameFromStream(in, 0L);
75
+ assertNotNull("Missing register response", regRespFrame);
76
+ assertEquals("Register command", JCoreWireCodec.CMD_REGISTER, regRespFrame.command);
77
+ assertEquals("Register status", 0, regRespFrame.statusCode);
78
+ JCoreWireCodec.RegisterResponse regResp = JCoreWireCodec.parseRegisterResponse(regRespFrame);
79
+ assertNotNull(regResp);
80
+ assertEquals(0L, (long) regResp.statusCode);
81
+ assertTrue(regResp.uid > 0L);
82
+ assertTrue(regResp.registrationId != null && !regResp.registrationId.isEmpty());
83
+
84
+ byte[] loginBody = JCoreWireCodec.buildLoginPayload(
85
+ regResp.password,
86
+ ReactNativeJpushProtocolTest.DEMO_TEST_APP_KEY,
87
+ regResp.deviceId,
88
+ "CN"
89
+ );
90
+ byte[] loginFrame = JCoreWireCodec.encodeFrame(
91
+ JCoreWireCodec.CMD_LOGIN,
92
+ JCoreWireCodec.VER_LOGIN,
93
+ 102L,
94
+ 0,
95
+ regResp.uid,
96
+ loginBody
97
+ );
98
+ out.write(loginFrame);
99
+ out.flush();
100
+
101
+ JCoreWireCodec.DecodedFrame loginRespFrame = JCoreWireCodec.readFrameFromStream(in, regResp.uid);
102
+ assertNotNull("Missing login response", loginRespFrame);
103
+ assertEquals("Login command", JCoreWireCodec.CMD_LOGIN, loginRespFrame.command);
104
+ assertEquals("Login status", 0, loginRespFrame.statusCode);
105
+ JCoreWireCodec.LoginResponse loginResp = JCoreWireCodec.parseLoginResponse(loginRespFrame);
106
+ assertNotNull(loginResp);
107
+ assertEquals(0L, (long) loginResp.statusCode);
108
+ assertTrue(loginResp.sessionId != 0);
109
+ }
110
+ }
@@ -0,0 +1,73 @@
1
+ package com.margelo.nitro.reactnativejpush;
2
+
3
+ import static org.junit.Assert.*;
4
+ import cn.jiguang.core.JCoreWireCodec;
5
+ import java.io.ByteArrayInputStream;
6
+ import java.io.ByteArrayOutputStream;
7
+ import java.io.IOException;
8
+ import java.io.InputStream;
9
+ import java.util.function.Supplier;
10
+ import org.junit.Test;
11
+
12
+ public class JPushHandshakeFailureTest {
13
+ @Test
14
+ public void networkFailureIsNotConvertedToSuccess() {
15
+ InputStream broken = new InputStream() {
16
+ @Override public int read() throws IOException {
17
+ throw new IOException("Synthetic connection loss");
18
+ }
19
+ };
20
+ assertThrows(IOException.class, () -> JPushCloudHandshakeTest.performHandshake(broken, new ByteArrayOutputStream()));
21
+ }
22
+
23
+ @Test
24
+ public void missingResponseAssertionIsNotSwallowed() {
25
+ assertThrows(AssertionError.class, () -> JPushCloudHandshakeTest.performHandshake(
26
+ new ByteArrayInputStream(new byte[0]), new ByteArrayOutputStream()
27
+ ));
28
+ }
29
+
30
+ @Test
31
+ public void rejectedRegistrationFailsTheHandshake() {
32
+ AssertionError failure = assertThrows(AssertionError.class, () -> JPushCloudHandshakeTest.performHandshake(
33
+ lazyReply(() -> JCoreWireCodec.encodeResponseFrame(
34
+ JCoreWireCodec.CMD_REGISTER, JCoreWireCodec.VER_REGISTER, 101L,
35
+ (int) JCoreWireCodec.getLastRegisterSeed(), 42L, 1009, new byte[0]
36
+ )), new ByteArrayOutputStream()
37
+ ));
38
+ assertTrue(failure.getMessage().contains("Register status"));
39
+ }
40
+
41
+ @Test
42
+ public void rejectedLoginFailsTheHandshake() {
43
+ AssertionError failure = assertThrows(AssertionError.class, () -> JPushCloudHandshakeTest.performHandshake(
44
+ lazyReply(() -> {
45
+ byte[] registration = JCoreWireCodec.encodeResponseFrame(
46
+ JCoreWireCodec.CMD_REGISTER, JCoreWireCodec.VER_REGISTER, 101L,
47
+ (int) JCoreWireCodec.getLastRegisterSeed(), 42L, 0,
48
+ JCoreWireCodec.buildRegisterResponsePayload(42L, "synthetic_password", "synthetic_registration", "synthetic_device")
49
+ );
50
+ byte[] login = JCoreWireCodec.encodeResponseFrame(
51
+ JCoreWireCodec.CMD_LOGIN, JCoreWireCodec.VER_LOGIN, 102L, 0, 42L, 1005, new byte[0]
52
+ );
53
+ byte[] replies = new byte[registration.length + login.length];
54
+ System.arraycopy(registration, 0, replies, 0, registration.length);
55
+ System.arraycopy(login, 0, replies, registration.length, login.length);
56
+ return replies;
57
+ }), new ByteArrayOutputStream()
58
+ ));
59
+ assertTrue(failure.getMessage().contains("Login status"));
60
+ }
61
+
62
+ private static InputStream lazyReply(Supplier<byte[]> response) {
63
+ return new InputStream() {
64
+ private ByteArrayInputStream input;
65
+ private ByteArrayInputStream input() {
66
+ if (input == null) input = new ByteArrayInputStream(response.get());
67
+ return input;
68
+ }
69
+ @Override public int read() { return input().read(); }
70
+ @Override public int read(byte[] bytes, int offset, int length) { return input().read(bytes, offset, length); }
71
+ };
72
+ }
73
+ }
@@ -200,114 +200,6 @@ public class ReactNativeJpushProtocolTest {
200
200
  assertTrue(propPayload.length > 2);
201
201
  }
202
202
 
203
- @Test
204
- public void testLiveJiguangCloudHandshakeWhenNetworkReachable() throws Exception {
205
- String sisJson = "";
206
- DatagramSocket udp = null;
207
- try {
208
- String reqJson = JCoreWireCodec.buildSisRequestJson(
209
- 1,
210
- DEMO_TEST_APP_KEY,
211
- 0L,
212
- "",
213
- Collections.<String>emptyList(),
214
- 1
215
- );
216
- byte[] reqPkt = JCoreWireCodec.encodeUdpSisPacket(reqJson, JCoreCrypto.nextPositiveSeed24());
217
- udp = new DatagramSocket();
218
- udp.setSoTimeout(2500);
219
- InetAddress addr = InetAddress.getByName("s.jpush.cn");
220
- udp.send(new DatagramPacket(reqPkt, reqPkt.length, addr, 19000));
221
- byte[] buf = new byte[4096];
222
- DatagramPacket respPkt = new DatagramPacket(buf, buf.length);
223
- udp.receive(respPkt);
224
- byte[] received = Arrays.copyOf(respPkt.getData(), respPkt.getLength());
225
- sisJson = JCoreWireCodec.decodeUdpSisPacket(received);
226
- } catch (Throwable networkUnavailable) {
227
- // Skip live cloud assertions when running in an offline or firewall-restricted CI container.
228
- return;
229
- } finally {
230
- if (udp != null) {
231
- udp.close();
232
- }
233
- }
234
-
235
- assertTrue(sisJson.contains("\"ips\""));
236
- Set<String> ips = JCoreWireCodec.extractJsonStringArray(sisJson, "ips");
237
- assertTrue(!ips.isEmpty());
238
-
239
- String firstIpPort = ips.iterator().next();
240
- int colon = firstIpPort.lastIndexOf(':');
241
- String host = firstIpPort.substring(0, colon);
242
- int port = Integer.parseInt(firstIpPort.substring(colon + 1));
243
-
244
- Socket tcp = new Socket();
245
- try {
246
- tcp.connect(new InetSocketAddress(host, port), 3000);
247
- tcp.setSoTimeout(5000);
248
- InputStream in = tcp.getInputStream();
249
- OutputStream out = tcp.getOutputStream();
250
-
251
- int seed = JCoreCrypto.nextRegisterSeed();
252
- JCoreWireCodec.setLastRegisterSeed(seed);
253
- byte[] regBody = JCoreWireCodec.buildRegisterPayload(
254
- DEMO_TEST_APP_KEY,
255
- "developer-default",
256
- DEMO_TEST_PACKAGE_NAME,
257
- "msgbench_auto_001"
258
- );
259
- byte[] regFrame = JCoreWireCodec.encodeFrame(
260
- JCoreWireCodec.CMD_REGISTER,
261
- JCoreWireCodec.VER_REGISTER,
262
- 101L,
263
- seed,
264
- 0L,
265
- regBody
266
- );
267
- out.write(regFrame);
268
- out.flush();
269
-
270
- JCoreWireCodec.DecodedFrame regRespFrame = JCoreWireCodec.readFrameFromStream(in, 0L);
271
- assertNotNull(regRespFrame);
272
- JCoreWireCodec.RegisterResponse regResp = JCoreWireCodec.parseRegisterResponse(regRespFrame);
273
- assertNotNull(regResp);
274
- assertEquals(0L, (long) regResp.statusCode);
275
- assertTrue(regResp.uid > 0L);
276
- assertTrue(regResp.registrationId != null && !regResp.registrationId.isEmpty());
277
-
278
- byte[] loginBody = JCoreWireCodec.buildLoginPayload(
279
- regResp.password,
280
- DEMO_TEST_APP_KEY,
281
- regResp.deviceId,
282
- "CN"
283
- );
284
- byte[] loginFrame = JCoreWireCodec.encodeFrame(
285
- JCoreWireCodec.CMD_LOGIN,
286
- JCoreWireCodec.VER_LOGIN,
287
- 102L,
288
- 0,
289
- regResp.uid,
290
- loginBody
291
- );
292
- out.write(loginFrame);
293
- out.flush();
294
-
295
- JCoreWireCodec.DecodedFrame loginRespFrame = JCoreWireCodec.readFrameFromStream(in, regResp.uid);
296
- assertNotNull(loginRespFrame);
297
- JCoreWireCodec.LoginResponse loginResp = JCoreWireCodec.parseLoginResponse(loginRespFrame);
298
- assertNotNull(loginResp);
299
- assertEquals(0L, (long) loginResp.statusCode);
300
- assertTrue(loginResp.sessionId != 0);
301
- } catch (Throwable tcpUnavailable) {
302
- // Ignore transient external network drop after UDP SIS succeeds.
303
- } finally {
304
- try {
305
- tcp.close();
306
- } catch (Throwable ignored) {
307
- }
308
- }
309
- }
310
-
311
203
  @Test
312
204
  public void testEmptyRegistrationBeforeHandshakeMultiRuntimeEventsAndManifestExportedFalse() throws Exception {
313
205
  // 1. Fresh state before TCP Register (cmd=0) must return "" for getRegistrationID (no fake 1a0018970a...)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-jpush",
3
- "version": "6.7.0-alpha.436",
3
+ "version": "6.7.0-alpha.445",
4
4
  "description": "Clean-room JPush and JCore React Native Nitro module for OneKey",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -94,7 +94,7 @@
94
94
  "typescript": "^5.9.2"
95
95
  },
96
96
  "peerDependencies": {
97
- "@onekeyfe/react-native-native-logger": "6.7.0-alpha.436",
97
+ "@onekeyfe/react-native-native-logger": "*",
98
98
  "react": "*",
99
99
  "react-native": "*",
100
100
  "react-native-nitro-modules": "0.37.0"