From 314c484ea072869bdf550ec1c66dec8136d6957a Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:01:31 +0500 Subject: [PATCH] mobile: fix note widget lookups corrupting the reminders list updateWidgetNote scanned every key in the appPreview preferences and rewrote any whose value contained the note id. That included the remindersList key, so a match replaced the entire reminders list with a note and then threw NumberFormatException parsing "remindersList" as a widget id. hasWidgetNote had the same unguarded scan. Match on the parsed note's id instead of on the raw JSON containing it, and treat only keys that parse as an int as widget entries, so non-widget keys are excluded structurally rather than by name. Also guards the casts and gson parses that could throw on a malformed entry. --- .../notesnook/NotePreviewWidget.java | 14 +----- .../notesnook/RCTNNativeModule.java | 41 +++++++--------- .../streetwriters/notesnook/WidgetUtils.java | 47 +++++++++++++++++++ 3 files changed, 66 insertions(+), 36 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java index 66c22778d..757187c67 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewWidget.java @@ -8,10 +8,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; -import android.util.Log; import android.view.View; import android.widget.RemoteViews; -import com.google.gson.Gson; import com.streetwriters.notesnook.datatypes.Note; import java.util.HashSet; @@ -22,18 +20,10 @@ public class NotePreviewWidget extends AppWidgetProvider { static String OpenNoteId = "com.streetwriters.notesnook.OpenNoteId"; static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { - String data = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), ""); + String data = context.getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), ""); RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget); - Note note = null; - if (data != null && !data.isEmpty()) { - try { - note = new Gson().fromJson(data, Note.class); - } catch (Exception e) { - Log.e("NotePreviewWidget", "Could not read the note stored for widget " + appWidgetId, e); - } - } - + Note note = WidgetUtils.parseNote(data); if (note == null) { // Either the widget was never configured, or we lost the note it pointed at (ids // reassigned, data cleared). Point it back at the picker rather than leaving the user diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java index e5872cfb5..cf21ecbdf 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/RCTNNativeModule.java @@ -29,7 +29,6 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; -import com.google.gson.Gson; import com.streetwriters.notesnook.datatypes.Note; import java.util.ArrayList; @@ -156,14 +155,8 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { @ReactMethod public void getWidgetNotes(Promise promise) { - SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Map map = pref.getAll(); WritableArray arr = Arguments.createArray(); - for(Map.Entry entry : map.entrySet()){ - if (entry.getKey().equals("remindersList")) continue; - String value = (String) entry.getValue(); - Gson gson = new Gson(); - Note note = gson.fromJson(value, Note.class); + for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) { arr.pushString(note.getId()); } promise.resolve(arr); @@ -171,33 +164,33 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { @ReactMethod public void hasWidgetNote(final String noteId, Promise promise) { - SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Map map = pref.getAll(); boolean found = false; - for(Map.Entry entry : map.entrySet()){ - String value = (String) entry.getValue(); - if (value.contains(noteId)) { + for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) { + if (note.getId().equals(noteId)) { found = true; + break; } } promise.resolve(found); } + @ReactMethod public void updateWidgetNote(final String noteId, final String data) { - SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Map map = pref.getAll(); + SharedPreferences pref = getReactApplicationContext().getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor edit = pref.edit(); - ArrayList ids = new ArrayList<>(); - for(Map.Entry entry : map.entrySet()) { - String value = (String) entry.getValue(); - if (value.contains(noteId)) { - edit.putString(entry.getKey(), data); - ids.add(entry.getKey()); - } + List ids = new ArrayList<>(); + + // Match on the note's id, not on the raw JSON containing it somewhere: a note whose body + // happens to mention another note's id is not the same note. + for (Map.Entry entry : WidgetUtils.getWidgetNotes(getReactApplicationContext()).entrySet()) { + if (!noteId.equals(entry.getValue().getId())) continue; + edit.putString(String.valueOf(entry.getKey()), data); + ids.add(entry.getKey()); } edit.apply(); - for (String id: ids) { - NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), Integer.parseInt(id)); + + for (int id : ids) { + NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), id); } } diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java index fa1d053a2..d3ab69a64 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java @@ -13,11 +13,14 @@ import android.widget.RemoteViews; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.streetwriters.notesnook.datatypes.Note; import com.streetwriters.notesnook.datatypes.Reminder; import java.util.ArrayList; import java.util.Calendar; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Shared helpers for the home screen widgets. @@ -33,6 +36,50 @@ public class WidgetUtils { */ private static final int MAX_REMINDERS = 50; + /** + * The note each note widget is showing, keyed by widget id. + * + * The preferences file mixes two things: one note per widget id, and the reminders list under + * its own key. Only numeric keys are widget notes, so anything else is skipped rather than + * being treated as a note. + */ + static Map getWidgetNotes(Context context) { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + Map notes = new LinkedHashMap<>(); + + for (Map.Entry entry : preferences.getAll().entrySet()) { + Integer widgetId = parseWidgetId(entry.getKey()); + if (widgetId == null) continue; + if (!(entry.getValue() instanceof String)) continue; + + Note note = parseNote((String) entry.getValue()); + if (note == null || note.getId() == null) continue; + notes.put(widgetId, note); + } + return notes; + } + + /** + * The widget id a preferences key refers to, or null if the key is not a widget id at all. + */ + private static Integer parseWidgetId(String key) { + try { + return Integer.valueOf(key); + } catch (NumberFormatException e) { + return null; + } + } + + static Note parseNote(String data) { + if (data == null || data.isEmpty()) return null; + try { + return new Gson().fromJson(data, Note.class); + } catch (Exception e) { + Log.e("NotePreviewWidget", "Could not read a stored note", e); + return null; + } + } + /** * The reminders the app last wrote out, minus any that have since fired. Reading and filtering * happens here so the provider can push the rows straight into the widget.