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 175a06da6..ac04cebcb 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 @@ -193,6 +193,16 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule { } } + /** + * Redraws every widget from scratch. Needed because the app can be stopped while its widgets + * stay on the home screen: clearing app data empties the store without the widgets ever being + * told, so they keep showing content that is gone until something forces a redraw. + */ + @ReactMethod + public void refreshWidgets() { + WidgetUtils.refreshAll(mContext); + } + @ReactMethod public void updateReminderWidget() { AppWidgetManager wm = AppWidgetManager.getInstance(mContext); 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 d3ab69a64..c04ec351c 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 @@ -1,6 +1,8 @@ package com.streetwriters.notesnook; import android.app.ActivityOptions; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -18,9 +20,11 @@ import com.streetwriters.notesnook.datatypes.Reminder; import java.util.ArrayList; import java.util.Calendar; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Shared helpers for the home screen widgets. @@ -36,6 +40,56 @@ public class WidgetUtils { */ private static final int MAX_REMINDERS = 50; + /** + * Redraws every widget that currently exists, and drops stored notes for widgets that no + * longer do. + * + * Everything else keys off what we have stored, which is fine while the app is running but + * leaves widgets showing content that no longer exists once the store is emptied underneath + * them (clearing app data) or a widget is removed while the app is stopped (onDeleted never + * arrives). Starting from the widgets the system knows about, rather than from our own data, + * is what makes this self-correcting. + * + * NoteWidget is left alone deliberately: it is a static button with no stored state, and its + * layout depends on the size it was last given. + */ + static void refreshAll(Context context) { + AppWidgetManager manager = AppWidgetManager.getInstance(context); + + int[] noteWidgetIds = manager.getAppWidgetIds( + new ComponentName(context, NotePreviewWidget.class)); + removeOrphanedNotes(context, noteWidgetIds); + for (int appWidgetId : noteWidgetIds) { + NotePreviewWidget.updateAppWidget(context, manager, appWidgetId); + } + + for (int appWidgetId : manager.getAppWidgetIds( + new ComponentName(context, ReminderWidgetProvider.class))) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders); + ReminderWidgetProvider.updateAppWidget(context, manager, appWidgetId, views); + } + } + + /** + * Drops stored notes whose widget is gone, so the preferences file cannot grow forever. + */ + private static void removeOrphanedNotes(Context context, int[] liveWidgetIds) { + Set live = new HashSet<>(); + for (int appWidgetId : liveWidgetIds) live.add(String.valueOf(appWidgetId)); + + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor edit = preferences.edit(); + boolean changed = false; + + for (String key : preferences.getAll().keySet()) { + // Leave anything that is not a widget id alone, the reminders list included. + if (parseWidgetId(key) == null || live.contains(key)) continue; + edit.remove(key); + changed = true; + } + if (changed) edit.apply(); + } + /** * The note each note widget is showing, keyed by widget id. * diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index 84bbd2e22..52f02f226 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -81,6 +81,7 @@ import { setUpdateAvailableMessage } from "../services/message"; import Navigation from "../services/navigation"; +import { NotePreviewWidget } from "../services/note-preview-widget"; import Notifications from "../services/notifications"; import PremiumService from "../services/premium"; import SettingsService from "../services/settings"; @@ -574,6 +575,11 @@ export const useAppEvents = () => { useEffect(() => { if (isAppLoading) return; + // Widgets outlive the app process, so they can be left showing content the app no longer has + // (most obviously after the user clears app data). Nothing can run at that moment, so the + // first launch afterwards is the earliest chance to put them right. + NotePreviewWidget.updateNotes(); + let subscriptions: EventManagerSubscription[] = []; const eventManager = db.eventManager; subscriptions = [ diff --git a/apps/mobile/app/services/note-preview-widget.ts b/apps/mobile/app/services/note-preview-widget.ts index 8f7610732..cbb259e6a 100644 --- a/apps/mobile/app/services/note-preview-widget.ts +++ b/apps/mobile/app/services/note-preview-widget.ts @@ -34,6 +34,10 @@ export const NotePreviewWidget = { NotesnookModule.updateWidgetNote(id, JSON.stringify(newNote)); } + // Redraw from the widgets that actually exist rather than only the ones we + // have notes for. After app data is cleared there are none, and the widgets + // would otherwise keep showing content that no longer exists. + NotesnookModule.refreshWidgets(); }, 500); }, updateNote: async (id: string, note: Note) => { diff --git a/apps/mobile/app/utils/notesnook-module.ts b/apps/mobile/app/utils/notesnook-module.ts index d682e3f28..0d90c4681 100644 --- a/apps/mobile/app/utils/notesnook-module.ts +++ b/apps/mobile/app/utils/notesnook-module.ts @@ -45,6 +45,7 @@ interface NotesnookModuleInterface { hasWidgetNote: (noteId: string) => Promise; updateWidgetNote: (noteId: string, data: string) => void; updateReminderWidget: () => void; + refreshWidgets: () => void; isGestureNavigationEnabled: () => boolean; addShortcut: ( id: string, @@ -81,6 +82,7 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({ hasWidgetNote: () => {}, updateWidgetNote: () => {}, updateReminderWidget: () => {}, + refreshWidgets: () => {}, isGestureNavigationEnabled: () => true, addShortcut: () => Promise.resolve(false), removeShortcut: () => Promise.resolve(false),