Skip to content

Commit 45c9899

Browse files
authored
chore: Merge 4.67.1 into master (#6828)
2 parents b089e80 + 66a2b06 commit 45c9899

File tree

12 files changed

+497
-86
lines changed

12 files changed

+497
-86
lines changed

android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ android {
9090
minSdkVersion rootProject.ext.minSdkVersion
9191
targetSdkVersion rootProject.ext.targetSdkVersion
9292
versionCode VERSIONCODE as Integer
93-
versionName "4.67.0"
93+
versionName "4.67.1"
9494
vectorDrawables.useSupportLibrary = true
9595
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
9696
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!

android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Lines changed: 206 additions & 19 deletions
Large diffs are not rendered by default.

android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import java.math.BigInteger;
1414

15+
import chat.rocket.reactnative.BuildConfig;
16+
1517
class RNCallback implements Callback {
1618
public void invoke(Object... args) {
1719

@@ -64,7 +66,7 @@ public Ejson() {
6466
* needs access to React-specific keystore resources. This means MMKV cannot be initialized
6567
* before React Native starts.
6668
*/
67-
private void ensureMMKVInitialized() {
69+
private synchronized void ensureMMKVInitialized() {
6870
if (initializationAttempted) {
6971
return;
7072
}
@@ -105,28 +107,112 @@ private void ensureMMKVInitialized() {
105107
}
106108

107109
public String getAvatarUri() {
108-
if (type == null) {
110+
if (sender == null || sender.username == null || sender.username.isEmpty()) {
111+
Log.w(TAG, "Cannot generate avatar URI: sender or username is null");
112+
return null;
113+
}
114+
115+
String server = serverURL();
116+
if (server == null || server.isEmpty()) {
117+
Log.w(TAG, "Cannot generate avatar URI: serverURL is null");
118+
return null;
119+
}
120+
121+
String userToken = token();
122+
String uid = userId();
123+
124+
if (userToken.isEmpty() || uid.isEmpty()) {
125+
Log.w(TAG, "Cannot generate avatar URI: missing auth credentials (token=" + !userToken.isEmpty() + ", uid=" + !uid.isEmpty() + ")");
109126
return null;
110127
}
111-
return serverURL() + "/avatar/" + this.sender.username + "?rc_token=" + token() + "&rc_uid=" + userId();
128+
129+
String uri = server + "/avatar/" + sender.username + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid;
130+
131+
if (BuildConfig.DEBUG) {
132+
Log.d(TAG, "Generated avatar URI for user: " + sender.username);
133+
}
134+
135+
return uri;
112136
}
113137

114138
public String token() {
115139
ensureMMKVInitialized();
116140
String userId = userId();
117-
if (mmkv != null && userId != null) {
118-
return mmkv.decodeString(TOKEN_KEY.concat(userId));
141+
142+
if (mmkv == null) {
143+
Log.e(TAG, "token() called but MMKV is null");
144+
return "";
145+
}
146+
147+
if (userId == null || userId.isEmpty()) {
148+
Log.w(TAG, "token() called but userId is null or empty");
149+
return "";
119150
}
120-
return "";
151+
152+
String key = TOKEN_KEY.concat(userId);
153+
if (BuildConfig.DEBUG) {
154+
Log.d(TAG, "Looking up token with key: " + key);
155+
}
156+
157+
String token = mmkv.decodeString(key);
158+
159+
if (token == null || token.isEmpty()) {
160+
Log.w(TAG, "No token found in MMKV for userId");
161+
} else if (BuildConfig.DEBUG) {
162+
Log.d(TAG, "Successfully retrieved token from MMKV");
163+
}
164+
165+
return token != null ? token : "";
121166
}
122167

123168
public String userId() {
124169
ensureMMKVInitialized();
125170
String serverURL = serverURL();
126-
if (mmkv != null && serverURL != null) {
127-
return mmkv.decodeString(TOKEN_KEY.concat(serverURL));
171+
String key = TOKEN_KEY.concat(serverURL);
172+
173+
if (mmkv == null) {
174+
Log.e(TAG, "userId() called but MMKV is null");
175+
return "";
128176
}
129-
return "";
177+
178+
if (serverURL == null) {
179+
Log.e(TAG, "userId() called but serverURL is null");
180+
return "";
181+
}
182+
183+
if (BuildConfig.DEBUG) {
184+
Log.d(TAG, "Looking up userId with key: " + key);
185+
}
186+
187+
String userId = mmkv.decodeString(key);
188+
189+
if (userId == null || userId.isEmpty()) {
190+
Log.w(TAG, "No userId found in MMKV for server: " + NotificationHelper.sanitizeUrl(serverURL));
191+
192+
// Only list keys in debug builds for diagnostics
193+
if (BuildConfig.DEBUG) {
194+
try {
195+
String[] allKeys = mmkv.allKeys();
196+
if (allKeys != null && allKeys.length > 0) {
197+
Log.d(TAG, "Available MMKV keys count: " + allKeys.length);
198+
// Log only keys that match the TOKEN_KEY pattern for security
199+
for (String k : allKeys) {
200+
if (k != null && k.startsWith("reactnativemeteor_usertoken")) {
201+
Log.d(TAG, "Found auth key: " + k);
202+
}
203+
}
204+
} else {
205+
Log.w(TAG, "MMKV has no keys stored");
206+
}
207+
} catch (Exception e) {
208+
Log.e(TAG, "Error listing MMKV keys", e);
209+
}
210+
}
211+
} else if (BuildConfig.DEBUG) {
212+
Log.d(TAG, "Successfully retrieved userId from MMKV");
213+
}
214+
215+
return userId != null ? userId : "";
130216
}
131217

132218
public String privateKey() {
@@ -158,4 +244,4 @@ static class Content {
158244
String kid;
159245
String iv;
160246
}
161-
}
247+
}
Lines changed: 149 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package chat.rocket.reactnative.notification;
22

33
import android.os.Bundle;
4+
import android.util.Log;
45

56
import com.facebook.react.bridge.ReactApplicationContext;
67
import com.google.gson.Gson;
8+
import com.google.gson.JsonSyntaxException;
9+
10+
import java.io.IOException;
11+
import java.util.concurrent.TimeUnit;
712

813
import okhttp3.HttpUrl;
914
import okhttp3.OkHttpClient;
1015
import okhttp3.Request;
1116
import okhttp3.Response;
17+
import okhttp3.ResponseBody;
1218

1319
class JsonResponse {
1420
Data data;
@@ -31,54 +37,163 @@ class Payload {
3137
String notificationType;
3238
String name;
3339
String messageType;
40+
String senderName;
41+
String msg;
42+
String tmid;
43+
Content content;
3444

3545
class Sender {
3646
String _id;
3747
String username;
3848
String name;
3949
}
50+
51+
class Content {
52+
String algorithm;
53+
String ciphertext;
54+
String kid;
55+
String iv;
56+
}
4057
}
4158
}
4259
}
4360
}
4461

4562
public class LoadNotification {
63+
private static final String TAG = "RocketChat.LoadNotif";
4664
private int RETRY_COUNT = 0;
4765
private int[] TIMEOUT = new int[]{0, 1, 3, 5, 10};
4866
private String TOKEN_KEY = "reactnativemeteor_usertoken-";
4967

5068
public void load(ReactApplicationContext reactApplicationContext, final Ejson ejson, Callback callback) {
51-
final OkHttpClient client = new OkHttpClient();
52-
HttpUrl.Builder url = HttpUrl.parse(ejson.serverURL().concat("/api/v1/push.get")).newBuilder();
69+
Log.i(TAG, "Starting notification load for message-id-only notification");
70+
71+
// Validate ejson object
72+
if (ejson == null) {
73+
Log.e(TAG, "Failed to load notification: ejson is null");
74+
callback.call(null);
75+
return;
76+
}
77+
78+
final String serverURL = ejson.serverURL();
79+
final String messageId = ejson.messageId;
80+
81+
Log.d(TAG, "Notification payload - serverURL: " + NotificationHelper.sanitizeUrl(serverURL) + ", messageId: " + (messageId != null ? "[present]" : "[null]"));
82+
83+
// Validate required fields
84+
if (serverURL == null || serverURL.isEmpty()) {
85+
Log.e(TAG, "Failed to load notification: serverURL is null or empty");
86+
callback.call(null);
87+
return;
88+
}
89+
90+
if (messageId == null || messageId.isEmpty()) {
91+
Log.e(TAG, "Failed to load notification: messageId is null or empty");
92+
callback.call(null);
93+
return;
94+
}
5395

5496
final String userId = ejson.userId();
5597
final String userToken = ejson.token();
5698

57-
if (userId == null || userToken == null) {
99+
if (userId == null || userId.isEmpty()) {
100+
Log.w(TAG, "Failed to load notification: userId is null or empty (user may not be logged in)");
101+
callback.call(null);
102+
return;
103+
}
104+
105+
if (userToken == null || userToken.isEmpty()) {
106+
Log.w(TAG, "Failed to load notification: userToken is null or empty (user may not be logged in)");
107+
callback.call(null);
108+
return;
109+
}
110+
111+
// Configure OkHttpClient with proper timeouts
112+
final OkHttpClient client = new OkHttpClient.Builder()
113+
.connectTimeout(10, TimeUnit.SECONDS)
114+
.readTimeout(30, TimeUnit.SECONDS)
115+
.writeTimeout(30, TimeUnit.SECONDS)
116+
.build();
117+
118+
HttpUrl.Builder urlBuilder;
119+
try {
120+
urlBuilder = HttpUrl.parse(serverURL.concat("/api/v1/push.get")).newBuilder();
121+
} catch (Exception e) {
122+
Log.e(TAG, "Failed to parse server URL: " + NotificationHelper.sanitizeUrl(serverURL), e);
123+
callback.call(null);
58124
return;
59125
}
60126

61127
Request request = new Request.Builder()
62128
.header("x-user-id", userId)
63129
.header("x-auth-token", userToken)
64-
.url(url.addQueryParameter("id", ejson.messageId).build())
130+
.url(urlBuilder.addQueryParameter("id", messageId).build())
65131
.build();
132+
133+
String sanitizedEndpoint = NotificationHelper.sanitizeUrl(serverURL) + "/api/v1/push.get";
134+
Log.d(TAG, "Built request to endpoint: " + sanitizedEndpoint);
66135

67-
runRequest(client, request, callback);
136+
runRequest(client, request, callback, sanitizedEndpoint);
68137
}
69138

70-
private void runRequest(OkHttpClient client, Request request, Callback callback) {
139+
private void runRequest(OkHttpClient client, Request request, Callback callback, String sanitizedEndpoint) {
71140
try {
72-
Thread.sleep(TIMEOUT[RETRY_COUNT] * 1000);
141+
int delay = TIMEOUT[RETRY_COUNT];
142+
if (delay > 0) {
143+
Log.d(TAG, "Retry attempt " + RETRY_COUNT + ", waiting " + delay + " seconds before request");
144+
} else {
145+
Log.d(TAG, "Attempt " + (RETRY_COUNT + 1) + ", executing request to " + sanitizedEndpoint);
146+
}
147+
148+
Thread.sleep(delay * 1000);
73149

74150
Response response = client.newCall(request).execute();
75-
String body = response.body().string();
151+
int statusCode = response.code();
152+
153+
ResponseBody responseBody = response.body();
154+
if (responseBody == null) {
155+
Log.e(TAG, "Request failed: response body is null (status: " + statusCode + ")");
156+
throw new IOException("Response body is null");
157+
}
158+
159+
String body = responseBody.string();
160+
76161
if (!response.isSuccessful()) {
77-
throw new Exception("Error");
162+
if (statusCode == 401 || statusCode == 403) {
163+
Log.w(TAG, "Authentication failed: HTTP " + statusCode + " - user may need to re-login");
164+
} else if (statusCode >= 500) {
165+
Log.e(TAG, "Server error: HTTP " + statusCode + " - server may be experiencing issues");
166+
} else {
167+
Log.w(TAG, "Request failed with HTTP " + statusCode);
168+
}
169+
throw new IOException("HTTP " + statusCode);
78170
}
171+
172+
Log.i(TAG, "Successfully received response (HTTP " + statusCode + "), parsing notification data");
79173

80174
Gson gson = new Gson();
81-
JsonResponse json = gson.fromJson(body, JsonResponse.class);
175+
JsonResponse json;
176+
try {
177+
json = gson.fromJson(body, JsonResponse.class);
178+
} catch (JsonSyntaxException e) {
179+
Log.e(TAG, "Failed to parse JSON response", e);
180+
throw e;
181+
}
182+
183+
// Validate parsed response structure
184+
if (json == null || json.data == null || json.data.notification == null) {
185+
Log.e(TAG, "Invalid response structure: missing required fields");
186+
throw new IllegalStateException("Invalid response structure");
187+
}
188+
189+
// Log encryption fields if present
190+
if (json.data.notification.payload != null) {
191+
boolean hasEncryption = json.data.notification.payload.msg != null || json.data.notification.payload.content != null;
192+
if (hasEncryption) {
193+
Log.d(TAG, "Notification contains encrypted content: msg=" + (json.data.notification.payload.msg != null) +
194+
", content=" + (json.data.notification.payload.content != null));
195+
}
196+
}
82197

83198
Bundle bundle = new Bundle();
84199
bundle.putString("notId", json.data.notification.notId);
@@ -87,15 +202,33 @@ private void runRequest(OkHttpClient client, Request request, Callback callback)
87202
bundle.putString("ejson", gson.toJson(json.data.notification.payload));
88203
bundle.putBoolean("notificationLoaded", true);
89204

205+
Log.i(TAG, "Successfully loaded and parsed notification data");
90206
callback.call(bundle);
91207

208+
} catch (IOException e) {
209+
Log.e(TAG, "Network error on attempt " + (RETRY_COUNT + 1) + ": " + e.getClass().getSimpleName() + " - " + e.getMessage());
210+
handleRetryOrFailure(client, request, callback, sanitizedEndpoint);
211+
} catch (JsonSyntaxException e) {
212+
Log.e(TAG, "JSON parsing error: " + e.getMessage());
213+
handleRetryOrFailure(client, request, callback, sanitizedEndpoint);
214+
} catch (InterruptedException e) {
215+
Log.e(TAG, "Request interrupted: " + e.getMessage());
216+
Thread.currentThread().interrupt(); // Restore interrupt status
217+
callback.call(null);
92218
} catch (Exception e) {
93-
if (RETRY_COUNT <= TIMEOUT.length) {
94-
RETRY_COUNT++;
95-
runRequest(client, request, callback);
96-
} else {
97-
callback.call(null);
98-
}
219+
Log.e(TAG, "Unexpected error on attempt " + (RETRY_COUNT + 1) + ": " + e.getClass().getSimpleName() + " - " + e.getMessage());
220+
handleRetryOrFailure(client, request, callback, sanitizedEndpoint);
221+
}
222+
}
223+
224+
private void handleRetryOrFailure(OkHttpClient client, Request request, Callback callback, String sanitizedEndpoint) {
225+
if (RETRY_COUNT < TIMEOUT.length - 1) {
226+
RETRY_COUNT++;
227+
Log.d(TAG, "Will retry request (attempt " + (RETRY_COUNT + 1) + " of " + TIMEOUT.length + ")");
228+
runRequest(client, request, callback, sanitizedEndpoint);
229+
} else {
230+
Log.e(TAG, "All retry attempts exhausted (" + TIMEOUT.length + " attempts). Notification load failed.");
231+
callback.call(null);
99232
}
100233
}
101234
}

0 commit comments

Comments
 (0)