-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix(android): Push notification crashing on some edge cases #6667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughCentralizes JSON parsing in CustomPushNotification by adding a shared Gson and a safeFromJson helper; replaces ad‑hoc Gson.fromJson calls with null-safe parsing and guards, adds sender/avatar fallbacks and null checks across notification build, decryption, style, icon selection, and dismissal/deduplication flows. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant OS as Android OS
participant RN as CustomPushNotification
participant JSON as safeFromJson()
participant UI as NotificationBuilder
OS->>RN: onReceived(push payload)
RN->>JSON: safeFromJson(payload.ejson)
alt parsed
RN->>RN: extract sender, avatar, type, message
else null/invalid
RN->>RN: use defaults (sender="Unknown", skip avatar)
end
RN->>RN: decrypt if loadedEjson present
RN->>UI: build notification (icons, MessagingStyle) with guarded fields
UI-->>OS: post notification
OS->>RN: dismissal/update
RN->>JSON: safeFromJson(notif.ejson)
RN->>RN: dedupe/dismiss with null-safe comparisons
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro Disabled knowledge base sources:
📒 Files selected for processing (1)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (3)
243-249: Fix substring bounds; avoid crash on short messages like "A:"substring(start, length) can throw StringIndexOutOfBounds when pos + 2 > length, and message may be null.
Apply:
- if (ejson != null && ejson.type != null && !ejson.type.equals("d")) { - int pos = message.indexOf(":"); - int start = pos == -1 ? 0 : pos + 2; - return message.substring(start, message.length()); - } - return message; + if (message == null) { + return null; // or "" if preferred + } + if (ejson != null && ejson.type != null && !"d".equals(ejson.type)) { + final int pos = message.indexOf(':'); + final int start = pos == -1 ? 0 : Math.min(message.length(), pos + 2); + return message.substring(start); + } + return message;
165-171: Null-safe server URL comparison to prevent NPEserverURL() may be null; calling equals() on it risks NPE.
Apply:
- if (ejson != null && notEjson != null && ejson.serverURL().equals(notEjson.serverURL())) { + if (ejson != null && notEjson != null + && android.text.TextUtils.equals(ejson.serverURL(), notEjson.serverURL())) {If you prefer, add
import android.text.TextUtils;and drop the fully-qualified name.
58-58: Harden notificationMessages against concurrent access (possible CME/crashes)onReceived and builder paths can interleave; iterating while mutating risks ConcurrentModificationException.
Apply:
- private static Map<String, List<Bundle>> notificationMessages = new HashMap<String, List<Bundle>>(); + private static Map<String, List<Bundle>> notificationMessages = new java.util.concurrent.ConcurrentHashMap<>();- if (notificationMessages.get(notId) == null) { - notificationMessages.put(notId, new ArrayList<Bundle>()); - } + notificationMessages.computeIfAbsent(notId, k -> new java.util.concurrent.CopyOnWriteArrayList<>());CopyOnWriteArrayList makes the iteration below safe even when other threads add items.
Also applies to: 87-89, 159-173
🧹 Nitpick comments (5)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (5)
157-157: Remove unused local Gson instancesThese locals are dead code after introducing safeFromJson.
Apply:
- Gson gson = new Gson();- Gson gson = new Gson();Also applies to: 276-276
96-101: Wrap decryption in try/catch to prevent rare crashesdecryptMessage could throw; today it’s unchecked.
Apply:
- if (loadedEjson != null && loadedEjson.msg != null) { - // Override message with the decrypted content - String decrypted = Encryption.shared.decryptMessage(loadedEjson, reactApplicationContext); - if (decrypted != null) { - bundle.putString("message", decrypted); - } - } + if (loadedEjson != null && loadedEjson.msg != null) { + try { + final String decrypted = Encryption.shared.decryptMessage(loadedEjson, reactApplicationContext); + if (decrypted != null) { + bundle.putString("message", decrypted); + } + } catch (Exception ex) { + android.util.Log.e("CustomPushNotification", "Message decryption failed", ex); + } + }
108-109: Simplify type checkIf notificationType is declared as String, instanceof is redundant; prefer constant-first equals.
Apply:
- if (loadedEjson != null && loadedEjson.notificationType instanceof String && loadedEjson.notificationType.equals("videoconf")) { + if (loadedEjson != null && "videoconf".equals(loadedEjson.notificationType)) {
103-103: Micro: avoid allocationSystem.currentTimeMillis() is cheaper than new Date().getTime().
Apply:
- bundle.putLong("time", new Date().getTime()); + bundle.putLong("time", System.currentTimeMillis());
330-333: Gate inline reply on valid Ejson, not just "{}" stringInvalid JSON still enables reply today; prefer parsing result.
Apply:
- String ejson = bundle.getString("ejson", "{}"); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || notId.equals("1") || ejson.equals("{}")) { + Ejson e = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || "1".equals(notId) || e == null) { return; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java(11 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: ESLint and Test / run-eslint-and-test
🔇 Additional comments (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)
46-47: Good: single, shared Gson instance (thread-safe).Centralizing and reusing a single Gson is correct and avoids per-call allocations.
| /** | ||
| * Safely parse JSON string to object with error handling. | ||
| * | ||
| * @param json JSON string to parse | ||
| * @param classOfT Target class type | ||
| * @param <T> Type parameter | ||
| * @return Parsed object, or null if parsing fails | ||
| */ | ||
| private static <T> T safeFromJson(String json, Class<T> classOfT) { | ||
| if (json == null || json.trim().isEmpty() || json.equals("{}")) { | ||
| return null; // no need to create a new instance | ||
| } | ||
|
|
||
| try { | ||
| return gson.fromJson(json, classOfT); | ||
| } catch (Exception e) { | ||
| android.util.Log.e("CustomPushNotification", | ||
| "Failed to parse JSON into " + classOfT.getSimpleName() + ": " + json, e); | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
🧩 Analysis chain
Nice helper; add unit tests and scan for remaining direct fromJson/new Gson
The helper is good; ensure no other call sites bypass it.
Run:
🏁 Script executed:
#!/bin/bash
# Find direct Gson usages that might bypass safeFromJson
rg -nP --type=java '\bnew\s+Gson\s*\(' -S
rg -nP --type=java '\bfromJson\s*\(' -S
# Find remaining raw "message-id-only" checks that may need null-safety parity
rg -nP --type=java '"message-id-only"\)|\.equals\("message-id-only"\)' -n
# Smoke-check for other extractMessage implementations
rg -nP --type=java '\bextractMessage\s*\(' -n -C2Length of output: 3794
Centralize JSON parsing: replace direct Gson/fromJson with safeFromJson; add unit tests
Helper is fine; several call sites still instantiate or call Gson.fromJson — convert them to safeFromJson (or justify local Gson) and add unit tests for null/empty JSON and the "message-id-only" flows.
- android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java — new Gson (line 80); gson.fromJson (line 81).
- android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java — new Gson (line 68); gson.fromJson at lines 138, 164, 198, 203.
- android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java — private static Gson (line 47); local new Gson at 157 and 276; safeFromJson/gson.fromJson inside helper at 394.
- android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java — new Gson (line 51); gson.fromJson (line 52).
🤖 Prompt for AI Agents
In
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
around lines 380-400, centralize JSON parsing by replacing all direct new
Gson()/gson.fromJson usages across the listed files with the safeFromJson
helper: update LoadNotification (lines ~80-81), Encryption (lines ~68, 138, 164,
198, 203), CustomPushNotification (remove local new Gson at ~157 and ~276 and
use the private static gson + safeFromJson), and ReplyBroadcast (lines ~51-52)
to call safeFromJson(jsonString, TargetClass.class) instead of constructing
local Gson or calling fromJson directly; remove redundant local Gson fields,
ensure callers handle null returns from safeFromJson (fall back to
message-id-only logic where required), and add unit tests covering
null/empty/"{}" JSON and message-id-only flows to verify behavior.
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (2)
151-174: Fix potential NPE in serverURL comparison; remove unused Gson.
new Gson()is unused here.ejson.serverURL().equals(notEjson.serverURL())can NPE if either serverURL is null. UseObjects.equals.- Gson gson = new Gson(); @@ - if (ejson != null && notEjson != null && ejson.serverURL().equals(notEjson.serverURL())) { + if (notEjson != null && java.util.Objects.equals(ejson.serverURL(), notEjson.serverURL())) {
243-250: extractMessage can crash on null message; add null-safe handling.
message.indexOf(":")will throw ifmessageis null. Also flip"d".equals(...)to avoid NPE.- if (ejson != null && ejson.type != null && !ejson.type.equals("d")) { - int pos = message.indexOf(":"); - int start = pos == -1 ? 0 : pos + 2; - return message.substring(start, message.length()); - } - return message; + final String m = message == null ? "" : message; + if (ejson != null && ejson.type != null && !"d".equals(ejson.type)) { + final int pos = m.indexOf(':'); + final int start = pos == -1 ? 0 : Math.min(m.length(), pos + 2); + return m.substring(start); + } + return m;
♻️ Duplicate comments (2)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (2)
389-403: Make safeFromJson robust to whitespace and reusable across classes.
- Treat
" {} "as empty too.- Consider moving this to a small shared util (e.g.,
JsonSafe) or make it package-visible so other classes can use it. Currentprivatescope blocks reuse, which contradicts the “replace across the codebase” goal.- private static <T> T safeFromJson(String json, Class<T> classOfT) { - if (json == null || json.trim().isEmpty() || json.equals("{}")) { + private static <T> T safeFromJson(String json, Class<T> classOfT) { + final String s = json == null ? null : json.trim(); + if (s == null || s.isEmpty() || "{}".equals(s)) { return null; // no need to create a new instance } try { - return gson.fromJson(json, classOfT); + return gson.fromJson(s, classOfT); } catch (Exception e) { android.util.Log.e( "CustomPushNotification", "Failed to parse JSON into " + classOfT.getSimpleName() + " (payload redacted).", e ); return null; } }
45-47: Sweep remaining direct Gson usages across the module; ensure all parsing goes through safeFromJson.There are still local
new Gson()instantiations here and (likely) in adjacent classes. Consolidate for consistency and testability.#!/bin/bash # Repo-wide scan for direct Gson usage that should be routed through safeFromJson rg -nP --type=java '\bnew\s+Gson\s*\(' -S rg -nP --type=java '\bfromJson\s*\(' -S | rg -v 'safeFromJson'I can raise a follow-up PR to extract
safeFromJsoninto a smallnotificationpackage util and update all call sites. Want me to draft it?Also applies to: 151-174, 276-276, 389-403
🧹 Nitpick comments (4)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (4)
95-101: Guard decrypt against null React context.
reactApplicationContextcan be null early in process lifetime; add a null-check to avoid crashes.- String decrypted = Encryption.shared.decryptMessage(loadedEjson, reactApplicationContext); + String decrypted = reactApplicationContext != null + ? Encryption.shared.decryptMessage(loadedEjson, reactApplicationContext) + : null;Can you confirm
reactApplicationContextis always set viasetReactContextbefore notifications can arrive on cold start?
261-270: InboxStyle path should also guard null message.Prevent passing null to
addLine.- String message = data.getString("message"); - messageStyle.addLine(message); + String message = data.getString("message"); + messageStyle.addLine(message != null ? message : "");
276-286: Remove unused local Gson in notificationStyle.This instance isn’t used.
- Gson gson = new Gson();
300-312: Harden senderName fallback.If
ejsonexists butsenderNameis null/empty, fall back to a safe default to avoid blank names in UI.- String senderName = ejson != null ? ejson.senderName : "Unknown"; + String senderName = (ejson != null && ejson.senderName != null && !ejson.senderName.isEmpty()) + ? ejson.senderName + : "Unknown"; @@ - String senderName = ejson != null ? ejson.senderName : "Unknown"; + String senderName = (ejson != null && ejson.senderName != null && !ejson.senderName.isEmpty()) + ? ejson.senderName + : "Unknown";
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java(11 hunks)
🔇 Additional comments (3)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (3)
45-47: Good move: single, reusable Gson instance.Centralizing the Gson instance reduces object churn and keeps parsing behavior consistent.
69-72: Null-safe gate for "message-id-only" is correct.This prevents NPEs on malformed payloads before attempting server load.
211-223: Icon path: good null-guards on avatarUri.Large icon is only set when available; Glide fallback in getAvatar() covers failures.
|
Android Build Available Rocket.Chat Experimental 4.65.0.107352 Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNStlxkiH412JATX3b3QdQ_b72KhKzGNOgE72f9mhFTEYa7TMRyaOFdpipnyAlIAQPcl3T-AgPrHyZvIAm4a |
OtavioStasiak
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
…rors gracefully - Introduced a static method `safeFromJson` to safely parse JSON strings into Ejson objects, returning null for invalid inputs. - Updated all instances of Gson parsing to use `safeFromJson`, ensuring null checks are in place to prevent potential NullPointerExceptions. - Enhanced error logging for JSON parsing failures.
…/CustomPushNotification.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
3641925 to
850b18b
Compare
Proposed changes
safeFromJsonto safely parse JSON strings into Ejson objects, returning null for invalid inputs.safeFromJson, ensuring null checks are in place to prevent potential NullPointerExceptions.Issue(s)
#6661
https://rocketchat.atlassian.net/browse/NATIVE-1026
How to test or reproduce
Screenshots
Types of changes
Checklist
Further comments
Summary by CodeRabbit