From 0939f0f8059c6ac4bca00fc942e1c5f6cb954877 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 09:56:18 +0500 Subject: [PATCH 01/20] mobile: migrated off the deprecated background-activity-launch mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API 36+ → MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE API 34–35 → MODE_BACKGROUND_ACTIVITY_START_ALLOWED (unchanged behaviour on older devices) < 34 → null --- .../notesnook/NotePreviewWidget.java | 14 +------- .../streetwriters/notesnook/NoteWidget.java | 14 +------- .../notesnook/ReminderWidgetProvider.java | 17 ++------- .../streetwriters/notesnook/WidgetUtils.java | 36 +++++++++++++++++++ 4 files changed, 40 insertions(+), 41 deletions(-) create mode 100644 apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java 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 831eaaa87..111a4b23a 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 @@ -1,6 +1,5 @@ package com.streetwriters.notesnook; -import android.app.ActivityOptions; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; @@ -8,7 +7,6 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.widget.RemoteViews; import com.google.gson.Gson; @@ -34,7 +32,7 @@ public class NotePreviewWidget extends AppWidgetProvider { intent.setAction(Intent.ACTION_VIEW); intent.putExtra(RCTNNativeModule.IntentType, "OpenNote"); intent.setData(Uri.parse("nn://note/" + note.getId())); - PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.open_note, pendingIntent); appWidgetManager.updateAppWidget(appWidgetId, views); @@ -45,16 +43,6 @@ public class NotePreviewWidget extends AppWidgetProvider { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); } - private static Bundle getActivityOptionsBundle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - return activityOptions.toBundle(); - } else - return null; - } - @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java index 6207f5ef1..6efe9ed1e 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NoteWidget.java @@ -1,12 +1,10 @@ package com.streetwriters.notesnook; -import android.app.ActivityOptions; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.widget.RemoteViews; @@ -24,20 +22,10 @@ public class NoteWidget extends AppWidgetProvider { static void setClickIntent(Context context, RemoteViews views) { Intent intent = new Intent(context, ShareActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.new_note, pendingIntent); } - private static Bundle getActivityOptionsBundle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - return activityOptions.toBundle(); - } else - return null; - } - @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java index 3ecd4daed..ec62755ca 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java @@ -1,14 +1,11 @@ package com.streetwriters.notesnook; -import android.app.ActivityOptions; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; -import android.os.Bundle; import android.widget.RemoteViews; public class ReminderWidgetProvider extends AppWidgetProvider { @@ -23,20 +20,10 @@ public class ReminderWidgetProvider extends AppWidgetProvider { } - private static Bundle getActivityOptionsBundle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - return activityOptions.toBundle(); - } else - return null; - } - static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, RemoteViews views) { Intent listview_intent_template = new Intent(context, MainActivity.class); listview_intent_template.setAction(Intent.ACTION_VIEW); - PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent); Intent new_reminder_intent = new Intent(context, MainActivity.class); @@ -44,7 +31,7 @@ public class ReminderWidgetProvider extends AppWidgetProvider { new_reminder_intent.setAction(Intent.ACTION_VIEW); new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder"); new_reminder_intent.setData(Uri.parse("https://app.notesnook.com/new_reminder")); - PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle()); + PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.add_button, pendingIntent2); Intent list_remote_adapter_intent = new Intent(context, ReminderViewsService.class); 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 new file mode 100644 index 000000000..0d6b5c7a8 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetUtils.java @@ -0,0 +1,36 @@ +package com.streetwriters.notesnook; + +import android.app.ActivityOptions; +import android.os.Build; +import android.os.Bundle; + +/** + * Shared helpers for the home screen widgets. + */ +public class WidgetUtils { + + /** + * Options attached to the PendingIntents our widgets hand to the launcher, opting the creator + * (us) in to background activity starts so a tap on the widget can bring up an activity. + * + * MODE_BACKGROUND_ACTIVITY_START_ALLOWED is deprecated since API 36 and Android 17 extends the + * background activity launch restrictions to IntentSender, so on API 36+ we use the narrower + * MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE instead. That is enough for widgets: the + * sender is the launcher, which is visible whenever the user taps the widget. + */ + static Bundle getActivityOptionsBundle() { + ActivityOptions activityOptions; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + } else { + return null; + } + return activityOptions.toBundle(); + } +} From 9b29503b1717d5bd1ea530dfeb80180c500fa9a1 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 10:08:16 +0500 Subject: [PATCH 02/20] mobile: added onRestored() that migrates each stored note in the appPreview prefs from its old widget ID key to the new one. --- .../android/app/src/main/AndroidManifest.xml | 1 + .../notesnook/NotePreviewWidget.java | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 4f46df45c..877df5eca 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -88,6 +88,7 @@ android:label="@string/note"> " + newKeys = new HashSet<>(); + for (int i = 0; i < count; i++) { + notes[i] = preferences.getString(String.valueOf(oldWidgetIds[i]), ""); + newKeys.add(String.valueOf(newWidgetIds[i])); + } + + SharedPreferences.Editor edit = preferences.edit(); + for (int i = 0; i < count; i++) { + String oldKey = String.valueOf(oldWidgetIds[i]); + if (!newKeys.contains(oldKey)) { + edit.remove(oldKey); + } + } + for (int i = 0; i < count; i++) { + if (notes[i].isEmpty()) continue; + edit.putString(String.valueOf(newWidgetIds[i]), notes[i]); + } + edit.apply(); + } + @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); From 343d2b013182717595aec1b2dbf595f74593b982 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 10:39:20 +0500 Subject: [PATCH 03/20] core: export getUpcomingReminderTime --- packages/core/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 68056d730..2d27f43f5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -30,6 +30,7 @@ export { type DatabaseUpdatedEvent } from "./database/index.js"; export { FilteredSelector } from "./database/sql-collection.js"; export { getUpcomingReminder, + getUpcomingReminderTime, formatReminderTime, isReminderToday, isReminderActive From 942c9287fe237b51bd382fd2e1e8417b229ba187 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 10:39:39 +0500 Subject: [PATCH 04/20] =?UTF-8?q?mobile:=20fix=20stale=20reminder=20times?= =?UTF-8?q?=20=E2=80=94=20render-time=20formatting=20+=20valid=20updatePer?= =?UTF-8?q?iodMillis=20+=20self-refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notesnook/ReminderViewsService.java | 21 ++++- .../notesnook/ReminderWidgetProvider.java | 1 + .../streetwriters/notesnook/WidgetUtils.java | 84 +++++++++++++++++++ .../notesnook/datatypes/Reminder.java | 52 ++++++------ .../app/src/main/res/values/strings.xml | 7 ++ .../main/res/xml/widget_reminders_info.xml | 2 +- apps/mobile/app/services/notifications.ts | 21 ++++- 7 files changed, 153 insertions(+), 35 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java index 5e9416e39..1d1a40c4a 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java @@ -42,11 +42,24 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor @Override public void onDataSetChanged() { - reminders.clear(); SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE); - Gson gson = new Gson(); - reminders = gson.fromJson(preferences.getString("remindersList","[]"), new TypeToken>(){}.getType()); + List stored = null; + try { + Gson gson = new Gson(); + stored = gson.fromJson(preferences.getString("remindersList", "[]"), new TypeToken>(){}.getType()); + } catch (Exception e) { + Log.e("Reminders", "Could not read the stored reminders list", e); + } + List updated = new ArrayList(); + if (stored != null) { + for (Reminder reminder : stored) { + if (WidgetUtils.isReminderActive(reminder)) { + updated.add(reminder); + } + } + } + reminders = updated; } @Override @@ -71,7 +84,7 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor if (!useMiniLayout) { views.setTextViewText(R.id.reminder_description, reminder.getDescription()); } - views.setTextViewText(R.id.reminder_time, reminder.getFormattedTime()); + views.setTextViewText(R.id.reminder_time, WidgetUtils.formatReminderTime(context, reminder)); final Intent fillInIntent = new Intent(); final Bundle extras = new Bundle(); extras.putString(ReminderViewsService.OpenReminderId, reminder.getId()); diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java index ec62755ca..dc3aa9ff8 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java @@ -16,6 +16,7 @@ public class ReminderWidgetProvider extends AppWidgetProvider { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders); updateAppWidget(context, appWidgetManager, appWidgetId, views); + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list_view); } } 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 0d6b5c7a8..109f01403 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,8 +1,14 @@ package com.streetwriters.notesnook; import android.app.ActivityOptions; +import android.content.Context; import android.os.Build; import android.os.Bundle; +import android.text.format.DateUtils; + +import com.streetwriters.notesnook.datatypes.Reminder; + +import java.util.Calendar; /** * Shared helpers for the home screen widgets. @@ -33,4 +39,82 @@ public class WidgetUtils { } return activityOptions.toBundle(); } + + /** + * Whether a reminder should still be listed. Mirrors isReminderActive() in the core package, + * which the app applies when it writes the list out. We re-check here because the stored list + * is only rewritten while the app runs, so one-off reminders would otherwise linger in the + * widget long after they fired. + */ + static boolean isReminderActive(Reminder reminder) { + if (reminder == null) return false; + if (reminder.isDisabled()) return false; + + long now = System.currentTimeMillis(); + if (reminder.getSnoozeUntil() > now) return true; + if (!"once".equals(reminder.getMode())) return true; + + long triggerDate = reminder.getTriggerDate() > 0 ? reminder.getTriggerDate() : reminder.getDate(); + return triggerDate > now; + } + + /** + * Builds the label shown under a reminder. The app sends us the absolute trigger time plus the + * parts that never change ("5:00 PM", "12-05-2026, 5:00 PM"); everything that depends on the + * current time is decided here so it stays right as the widget redraws. + * + * Falls back to the pre-formatted string for lists written by an older version of the app. + */ + static String formatReminderTime(Context context, Reminder reminder) { + long triggerDate = reminder.getTriggerDate(); + String timeOfDay = reminder.getFormattedTimeOfDay(); + if (triggerDate <= 0 || timeOfDay == null || timeOfDay.isEmpty()) { + return reminder.getFormattedTime(); + } + + if ("permanent".equals(reminder.getMode())) { + return context.getString(R.string.reminder_ongoing); + } + + long now = System.currentTimeMillis(); + if (reminder.getSnoozeUntil() > now) { + return context.getString(R.string.reminder_snoozed_until, timeOfDay); + } + + String text; + long dayOffset = daysFromToday(triggerDate, now); + if (dayOffset == 0) { + text = context.getString(R.string.reminder_today, timeOfDay); + } else if (dayOffset == 1) { + text = context.getString(R.string.reminder_tomorrow, timeOfDay); + } else if (dayOffset == -1) { + text = context.getString(R.string.reminder_yesterday, timeOfDay); + } else { + text = reminder.getFormattedDateTime(); + if (text == null || text.isEmpty()) return reminder.getFormattedTime(); + } + + return context.getString( + triggerDate <= now ? R.string.reminder_last : R.string.reminder_upcoming, text); + } + + /** + * Calendar days between two instants. Compares midnights rather than subtracting the raw + * difference so that "tomorrow" is still tomorrow across a DST change or just before midnight. + */ + private static long daysFromToday(long time, long now) { + long target = startOfDay(time); + long today = startOfDay(now); + return Math.round((target - today) / (double) DateUtils.DAY_IN_MILLIS); + } + + private static long startOfDay(long time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(time); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTimeInMillis(); + } } diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java index 715199ca6..9b9b0fac6 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/datatypes/Reminder.java @@ -2,13 +2,14 @@ package com.streetwriters.notesnook.datatypes; import androidx.annotation.Keep; -import java.util.concurrent.TimeUnit; - @Keep public class Reminder extends BaseItem { private String title; private String description; private String formattedTime; + private String formattedTimeOfDay; // e.g. "5:00 PM" + private String formattedDateTime; // e.g. "12-05-2026, 5:00 PM" + private long triggerDate; // absolute time this reminder next fires private String priority; // "silent", "vibrate", "urgent" private long date; private String mode; // "repeat", "once", "permanent" @@ -107,32 +108,27 @@ public class Reminder extends BaseItem { this.formattedTime = formattedTime; } - public String formatTime(long timeInMillis) { - long currentTime = System.currentTimeMillis(); - long diff = timeInMillis - currentTime; + public String getFormattedTimeOfDay() { + return formattedTimeOfDay; + } - if (diff < TimeUnit.MINUTES.toMillis(1)) { - return "in " + (diff / 1000) + " seconds"; - } else if (diff < TimeUnit.HOURS.toMillis(1)) { - long minutes = TimeUnit.MILLISECONDS.toMinutes(diff); - return "in " + minutes + " minute" + (minutes > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(1)) { - long hours = TimeUnit.MILLISECONDS.toHours(diff); - return "in " + hours + " hour" + (hours > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(2)) { - return "tomorrow"; - } else if (diff < TimeUnit.DAYS.toMillis(7)) { - long days = TimeUnit.MILLISECONDS.toDays(diff); - return "in " + days + " day" + (days > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(30)) { - long weeks = TimeUnit.MILLISECONDS.toDays(diff) / 7; - return "in " + weeks + " week" + (weeks > 1 ? "s" : ""); - } else if (diff < TimeUnit.DAYS.toMillis(365)) { - long months = TimeUnit.MILLISECONDS.toDays(diff) / 30; - return "in " + months + " month" + (months > 1 ? "s" : ""); - } else { - long years = TimeUnit.MILLISECONDS.toDays(diff) / 365; - return "in " + years + " year" + (years > 1 ? "s" : ""); - } + public void setFormattedTimeOfDay(String formattedTimeOfDay) { + this.formattedTimeOfDay = formattedTimeOfDay; + } + + public String getFormattedDateTime() { + return formattedDateTime; + } + + public void setFormattedDateTime(String formattedDateTime) { + this.formattedDateTime = formattedDateTime; + } + + public long getTriggerDate() { + return triggerDate; + } + + public void setTriggerDate(long triggerDate) { + this.triggerDate = triggerDate; } } \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index 1a233ee09..a9ab7f599 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -9,4 +9,11 @@ Note Add a note to home screen Quick note + Ongoing + Snoozed until %1$s + Today, %1$s + Tomorrow, %1$s + Yesterday, %1$s + Upcoming: %1$s + Last: %1$s diff --git a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml index e9c87362f..d88c14187 100644 --- a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml @@ -9,6 +9,6 @@ android:targetCellHeight="2" android:resizeMode="horizontal|vertical" android:previewImage="@drawable/reminder_preview" - android:updatePeriodMillis="1024" + android:updatePeriodMillis="1800000" android:widgetCategory="home_screen" /> \ No newline at end of file diff --git a/apps/mobile/app/services/notifications.ts b/apps/mobile/app/services/notifications.ts index 73ffa8e45..389beca39 100644 --- a/apps/mobile/app/services/notifications.ts +++ b/apps/mobile/app/services/notifications.ts @@ -17,8 +17,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { getFormattedReminderTime } from "@notesnook/common"; -import { isReminderActive, Reminder } from "@notesnook/core"; +import { getFormattedDate, getFormattedReminderTime } from "@notesnook/common"; +import { + getUpcomingReminderTime, + isReminderActive, + Reminder +} from "@notesnook/core"; import { strings } from "@notesnook/intl"; import notifee, { AndroidStyle, @@ -262,6 +266,9 @@ const onEvent = async ({ type, detail }: Event) => { type ReminderWithFormattedTime = Reminder & { formattedTime?: string; + triggerDate?: number; + formattedTimeOfDay?: string; + formattedDateTime?: string; }; async function updateRemindersForWidget() { @@ -277,6 +284,16 @@ async function updateRemindersForWidget() { if (!reminders) return; for (const reminder of reminders) { if (isReminderActive(reminder)) { + const triggerDate = + reminder.snoozeUntil && reminder.snoozeUntil > Date.now() + ? reminder.snoozeUntil + : reminder.mode === "repeat" + ? getUpcomingReminderTime(reminder) + : reminder.date; + + reminder.triggerDate = triggerDate; + reminder.formattedTimeOfDay = getFormattedDate(triggerDate, "time"); + reminder.formattedDateTime = getFormattedDate(triggerDate, "date-time"); reminder.formattedTime = getFormattedReminderTime(reminder); activeReminders.push(reminder); } From 88eb3ab714eb88c71b8a0aa0709e7611a99ad800 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 10:49:36 +0500 Subject: [PATCH 05/20] =?UTF-8?q?mobile:=20fix=20NotePreviewWidget=20empty?= =?UTF-8?q?=20state=20has=20no=20click=20handler=20=E2=80=94=20make=20it?= =?UTF-8?q?=20tap-to-configure=20so=20a=20blank=20widget=20can=20self-heal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotePreviewConfigureActivity.java | 34 ++++++++++----- .../notesnook/NotePreviewWidget.java | 41 +++++++++++++++++-- .../app/src/main/res/layout/note_widget.xml | 4 +- .../app/src/main/res/values/strings.xml | 2 + 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java index a87a1f743..4418ce56f 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java @@ -40,18 +40,32 @@ public class NotePreviewConfigureActivity extends ReactActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(null); - Intent intent = getIntent(); - Bundle extras = intent.getExtras(); - int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; - if (extras != null) { - appWidgetId = extras.getInt( - AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID); - NotePreviewConfigureActivity.appWidgetId = appWidgetId; - } + activity = this; + readAppWidgetId(getIntent()); + } + + /** + * We launch as singleTask, so configuring a second widget while this screen is still alive + * arrives here rather than in onCreate(). Without this the activity would keep writing to + * whichever widget it happened to be opened for first. + */ + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + activity = this; + readAppWidgetId(intent); + } + + private void readAppWidgetId(Intent intent) { + Bundle extras = intent != null ? intent.getExtras() : null; + int appWidgetId = extras != null + ? extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + : AppWidgetManager.INVALID_APPWIDGET_ID; + + NotePreviewConfigureActivity.appWidgetId = appWidgetId; Intent resultValue = new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); setResult(Activity.RESULT_CANCELED, resultValue); - activity = this; } public static void saveAndFinish(Context context) { 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 f153e9f3b..14117db71 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,6 +8,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.util.Log; import android.widget.RemoteViews; import com.google.gson.Gson; import com.streetwriters.notesnook.datatypes.Note; @@ -21,12 +22,28 @@ public class NotePreviewWidget extends AppWidgetProvider { static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { String data = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), ""); - if (data.isEmpty()) { + 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); + } + } + + 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 + // with an inert widget they can only fix by deleting and re-adding it. + views.setTextViewText(R.id.widget_title, context.getString(R.string.widget_note_unconfigured_title)); + views.setTextViewText(R.id.widget_body, context.getString(R.string.widget_note_unconfigured_body)); + views.setOnClickPendingIntent(R.id.open_note, getConfigurePendingIntent(context, appWidgetId)); + appWidgetManager.updateAppWidget(appWidgetId, views); return; } - Gson gson = new Gson(); - Note note = gson.fromJson(data, Note.class); - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget); + views.setTextViewText(R.id.widget_title, note.getTitle()); views.setTextViewText(R.id.widget_body, note.getHeadline()); @@ -41,6 +58,22 @@ public class NotePreviewWidget extends AppWidgetProvider { appWidgetManager.updateAppWidget(appWidgetId, views); } + /** + * Reopens the configure screen for this widget. The launcher's own "reconfigure" gesture is + * hard to discover and not offered by every launcher, so an unconfigured widget needs its own + * way back in. + */ + private static PendingIntent getConfigurePendingIntent(Context context, int appWidgetId) { + Intent intent = new Intent(context, NotePreviewConfigureActivity.class); + intent.setAction(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + // PendingIntent equality ignores extras, so the widget id has to be the request code for + // each widget to get its own. + return PendingIntent.getActivity(context, appWidgetId, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, + WidgetUtils.getActivityOptionsBundle()); + } + @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); diff --git a/apps/mobile/android/app/src/main/res/layout/note_widget.xml b/apps/mobile/android/app/src/main/res/layout/note_widget.xml index 8a97ce0d3..2eecacfb2 100644 --- a/apps/mobile/android/app/src/main/res/layout/note_widget.xml +++ b/apps/mobile/android/app/src/main/res/layout/note_widget.xml @@ -25,7 +25,7 @@ android:textColor="@color/text" android:textSize="16sp" android:textStyle="bold" - android:text="Widget unconfigured" /> + android:text="@string/widget_note_unconfigured_title" /> + android:text="@string/widget_note_unconfigured_body" /> \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index a9ab7f599..7a86c8d38 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -9,6 +9,8 @@ Note Add a note to home screen Quick note + Tap to choose a note + Pick the note you want shown here. Ongoing Snoozed until %1$s Today, %1$s From b2c7f732a0d71746bd7bd72c835959b6ea47947e Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 11:10:46 +0500 Subject: [PATCH 06/20] mobile: replace reminders widget service with RemoteCollectionItems Push the reminder rows into the widget update itself via RemoteViewsCompat instead of binding a RemoteViewsService adapter, which has been deprecated since API 31. Removes the bound service entirely, so there is no cached factory to invalidate and no separate notifyAppWidgetViewDataChanged step that can race or be missed. Adds androidx.core:core-remoteviews for the API 24-30 fallback. Row ids are now derived from the reminder id rather than the list position, and the list is capped at 50 rows so the update fits in a binder transaction. --- apps/mobile/android/app/build.gradle | 3 + .../android/app/src/main/AndroidManifest.xml | 5 - .../notesnook/RCTNNativeModule.java | 4 +- .../notesnook/ReminderViewsService.java | 119 ------------------ .../notesnook/ReminderWidgetProvider.java | 34 ++++- .../streetwriters/notesnook/WidgetUtils.java | 69 ++++++++++ 6 files changed, 103 insertions(+), 131 deletions(-) delete mode 100644 apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderViewsService.java diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 7cc709a78..1f4781422 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -233,6 +233,9 @@ dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") + // Lets the widgets push their list data straight into the RemoteViews on every API level, + // instead of the deprecated RemoteViewsService adapter (which needs API 31 to do natively). + implementation("androidx.core:core-remoteviews:1.0.0") implementation("androidx.core:core-splashscreen:1.0.0") implementation 'androidx.multidex:multidex:2.0.1' diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 877df5eca..fc7f88466 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -231,11 +231,6 @@ - - reminders; - - public ReminderRemoteViewsFactory(Context context, Intent intent) { - this.context = context; - } - - @Override - public void onCreate() { - // Initialize reminders list - reminders = new ArrayList(); - } - - @Override - public void onDataSetChanged() { - SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE); - List stored = null; - try { - Gson gson = new Gson(); - stored = gson.fromJson(preferences.getString("remindersList", "[]"), new TypeToken>(){}.getType()); - } catch (Exception e) { - Log.e("Reminders", "Could not read the stored reminders list", e); - } - - List updated = new ArrayList(); - if (stored != null) { - for (Reminder reminder : stored) { - if (WidgetUtils.isReminderActive(reminder)) { - updated.add(reminder); - } - } - } - reminders = updated; - } - - @Override - public void onDestroy() { - reminders.clear(); - } - - @Override - public int getCount() { - return reminders.size(); - } - - @Override - public RemoteViews getViewAt(int position) { - Reminder reminder = reminders.get(position); - - boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty(); - - RemoteViews views = new RemoteViews(context.getPackageName(), useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout); - - views.setTextViewText(R.id.reminder_title, reminder.getTitle()); - if (!useMiniLayout) { - views.setTextViewText(R.id.reminder_description, reminder.getDescription()); - } - views.setTextViewText(R.id.reminder_time, WidgetUtils.formatReminderTime(context, reminder)); - final Intent fillInIntent = new Intent(); - final Bundle extras = new Bundle(); - extras.putString(ReminderViewsService.OpenReminderId, reminder.getId()); - fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId())); - fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder"); - fillInIntent.putExtras(extras); - views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent); - return views; - } - - - @Override - public RemoteViews getLoadingView() { - return null; - } - - @Override - public int getViewTypeCount() { - return 2; - } - - @Override - public long getItemId(int position) { - - return position; - } - - @Override - public boolean hasStableIds() { - return true; - } -} \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java index dc3aa9ff8..4619f594c 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java @@ -8,15 +8,21 @@ import android.content.Intent; import android.net.Uri; import android.widget.RemoteViews; +import androidx.core.widget.RemoteViewsCompat; + +import com.streetwriters.notesnook.datatypes.Reminder; + +import java.util.List; + public class ReminderWidgetProvider extends AppWidgetProvider { static String NewReminder = "com.streetwriters.notesnook.NewReminder"; + static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId"; @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders); updateAppWidget(context, appWidgetManager, appWidgetId, views); - appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list_view); } } @@ -35,10 +41,28 @@ public class ReminderWidgetProvider extends AppWidgetProvider { PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle()); views.setOnClickPendingIntent(R.id.add_button, pendingIntent2); - Intent list_remote_adapter_intent = new Intent(context, ReminderViewsService.class); - list_remote_adapter_intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - views.setRemoteAdapter(R.id.widget_list_view, list_remote_adapter_intent); + // The rows travel with the update itself, so there is no bound service to keep in sync and + // nothing to invalidate separately: every update redraws from the current data. + List reminders = WidgetUtils.getActiveReminders(context); + RemoteViewsCompat.RemoteCollectionItems.Builder items = + new RemoteViewsCompat.RemoteCollectionItems.Builder(); + for (Reminder reminder : reminders) { + items.addItem(getItemId(reminder), WidgetUtils.createReminderItem(context, reminder)); + } + // Two, because a reminder without a description uses the compact row layout. + items.setViewTypeCount(2); + items.setHasStableIds(true); + + RemoteViewsCompat.setRemoteAdapter(context, views, appWidgetId, R.id.widget_list_view, items.build()); views.setEmptyView(R.id.widget_list_view, R.id.empty_view); appWidgetManager.updateAppWidget(appWidgetId, views); } -} \ No newline at end of file + + /** + * Ties a row to its reminder rather than to its position, so rows keep their identity when the + * list shifts around them. + */ + private static long getItemId(Reminder reminder) { + return reminder.getId() == null ? 0 : reminder.getId().hashCode(); + } +} 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 109f01403..fa1d053a2 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 @@ -2,19 +2,88 @@ package com.streetwriters.notesnook; import android.app.ActivityOptions; import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.format.DateUtils; +import android.util.Log; +import android.widget.RemoteViews; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import com.streetwriters.notesnook.datatypes.Reminder; +import java.util.ArrayList; import java.util.Calendar; +import java.util.List; /** * Shared helpers for the home screen widgets. */ public class WidgetUtils { + static final String PREFERENCES = "appPreview"; + static final String REMINDERS_KEY = "remindersList"; + + /** + * Every row is serialized into the widget update itself, which has to fit inside a binder + * transaction, so the list cannot grow without bound. Far more than fits on screen anyway. + */ + private static final int MAX_REMINDERS = 50; + + /** + * 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. + */ + static List getActiveReminders(Context context) { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + List stored = null; + try { + stored = new Gson().fromJson(preferences.getString(REMINDERS_KEY, "[]"), + new TypeToken>() {}.getType()); + } catch (Exception e) { + Log.e("Reminders", "Could not read the stored reminders list", e); + } + + List active = new ArrayList<>(); + if (stored == null) return active; + + for (Reminder reminder : stored) { + if (!isReminderActive(reminder)) continue; + if (active.size() >= MAX_REMINDERS) { + Log.w("Reminders", "Widget list truncated to " + MAX_REMINDERS + " reminders"); + break; + } + active.add(reminder); + } + return active; + } + + /** + * Builds a single row of the reminders list. + */ + static RemoteViews createReminderItem(Context context, Reminder reminder) { + boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty(); + + RemoteViews views = new RemoteViews(context.getPackageName(), + useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout); + + views.setTextViewText(R.id.reminder_title, reminder.getTitle()); + if (!useMiniLayout) { + views.setTextViewText(R.id.reminder_description, reminder.getDescription()); + } + views.setTextViewText(R.id.reminder_time, formatReminderTime(context, reminder)); + + Intent fillInIntent = new Intent(); + fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId())); + fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder"); + fillInIntent.putExtra(ReminderWidgetProvider.OpenReminderId, reminder.getId()); + views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent); + return views; + } + /** * Options attached to the PendingIntents our widgets hand to the launcher, opting the creator * (us) in to background activity starts so a tap on the widget can bring up an activity. From 55b51b81e364e3b6affc9dbb4313a52f2cf4e0ba Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 11:17:12 +0500 Subject: [PATCH 07/20] mobile: add previewLayout for all widgets --- .../main/res/layout/note_widget_preview.xml | 42 ++++++++++ .../res/layout/widget_reminders_preview.xml | 82 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 8 ++ .../src/main/res/xml/new_note_widget_info.xml | 1 + .../app/src/main/res/xml/note_widget_info.xml | 1 + .../main/res/xml/widget_reminders_info.xml | 1 + 6 files changed, 135 insertions(+) create mode 100644 apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml create mode 100644 apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml diff --git a/apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml b/apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml new file mode 100644 index 000000000..710b74aa9 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/layout/note_widget_preview.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml new file mode 100644 index 000000000..2208f51fd --- /dev/null +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders_preview.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index 7a86c8d38..8652b200d 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -9,6 +9,14 @@ Note Add a note to home screen Quick note + + Meeting notes + Discuss the roadmap and agree on timelines. + Take a walk + Upcoming: Today, 5:00 PM + Call the dentist + Upcoming: Tomorrow, 9:00 AM + Tap to choose a note Pick the note you want shown here. Ongoing diff --git a/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml b/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml index 086bf7228..c76d158b6 100644 --- a/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/new_note_widget_info.xml @@ -10,6 +10,7 @@ android:targetCellWidth="5" android:targetCellHeight="1" android:previewImage="@drawable/widget_preview" + android:previewLayout="@layout/new_note_widget" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen"/> \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml b/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml index 84c936b29..c597dc450 100644 --- a/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/note_widget_info.xml @@ -12,6 +12,7 @@ android:targetCellWidth="5" android:targetCellHeight="1" android:previewImage="@drawable/note_widget_preview" + android:previewLayout="@layout/note_widget_preview" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen"/> \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml index d88c14187..0d7068b1c 100644 --- a/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml +++ b/apps/mobile/android/app/src/main/res/xml/widget_reminders_info.xml @@ -9,6 +9,7 @@ android:targetCellHeight="2" android:resizeMode="horizontal|vertical" android:previewImage="@drawable/reminder_preview" + android:previewLayout="@layout/widget_reminders_preview" android:updatePeriodMillis="1800000" android:widgetCategory="home_screen" /> \ No newline at end of file From 6b5e6f62c5fd2ff10892bf2376e911da57b54461 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 11:25:34 +0500 Subject: [PATCH 08/20] mobile: make NotePreviewWidget respond to resize events and properly resize based on available room. --- .../notesnook/NotePreviewWidget.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 14117db71..66c22778d 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 @@ -9,6 +9,7 @@ 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; @@ -46,6 +47,10 @@ public class NotePreviewWidget extends AppWidgetProvider { views.setTextViewText(R.id.widget_title, note.getTitle()); views.setTextViewText(R.id.widget_body, note.getHeadline()); + // Once the user shrinks the widget down to a single row there is no room for the preview + // text, and a clipped half-line of it looks like a rendering glitch. + views.setViewVisibility(R.id.widget_body, + hasRoomForBody(appWidgetManager, appWidgetId) ? View.VISIBLE : View.GONE); Intent intent = new Intent(context, MainActivity.class); intent.putExtra(OpenNoteId, note.getId()); @@ -74,9 +79,25 @@ public class NotePreviewWidget extends AppWidgetProvider { WidgetUtils.getActivityOptionsBundle()); } + /** + * Height below which the note preview text is dropped, leaving just the title. + */ + private static final int MIN_HEIGHT_FOR_BODY_DP = 70; + + private static boolean hasRoomForBody(AppWidgetManager appWidgetManager, int appWidgetId) { + Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId); + if (options == null) return true; + + int minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT); + // Not reported yet (the widget has just been placed): assume there is room. + return minHeight <= 0 || minHeight >= MIN_HEIGHT_FOR_BODY_DP; + } + @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); + // This used to do nothing at all, so resizing the widget left it rendered for its old size. + updateAppWidget(context, appWidgetManager, appWidgetId); } /** From 314c484ea072869bdf550ec1c66dec8136d6957a Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:01:31 +0500 Subject: [PATCH 09/20] 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. From 1889b1345976fa0b566b7f657b2ab075009299ec Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:05:28 +0500 Subject: [PATCH 10/20] mobile: remove stray quote in NotePreviewWidget manifest entry --- apps/mobile/android/app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index fc7f88466..4b6724133 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -86,7 +86,7 @@ android:name=".NotePreviewWidget" android:exported="false" android:label="@string/note"> - " + From 70fd3096d2e269ba2d584e017028ff76cdacfa0a Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:10:25 +0500 Subject: [PATCH 11/20] mobile: fix invisible empty state in the reminders widget --- .../app/src/main/res/layout/widget_reminder_empty.xml | 8 -------- .../android/app/src/main/res/layout/widget_reminders.xml | 3 ++- apps/mobile/android/app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml deleted file mode 100644 index 9be87bb7a..000000000 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminder_empty.xml +++ /dev/null @@ -1,8 +0,0 @@ - \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml index f7f25bf55..c7bc790c5 100644 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml @@ -57,7 +57,8 @@ android:layout_height="match_parent" android:textAlignment="center" android:gravity="center" - android:text="Tap on + to add reminder"/> + android:textColor="@color/text" + android:text="@string/widget_reminders_empty"/> diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index 8652b200d..51cd67101 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -9,6 +9,8 @@ Note Add a note to home screen Quick note + Tap + to add a reminder + Meeting notes Discuss the roadmap and agree on timelines. From 93337b6560741ed6d08b1fb4acf7adb2ca7d32e0 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:14:13 +0500 Subject: [PATCH 12/20] mobile: remove dead widget theme overlay, use system corner radius ThemeOverlay.Notesnook.AppWidgetContainer declared and set appWidgetBackgroundColor/appWidgetTextColor, but no layout ever read them - leftovers from the Android Studio widget template, in a palette unrelated to the app. Removes the overlay, its declare-styleable, the android:theme references, and the light_blue_* colors that only it used. Also replaces the hardcoded 10dp widget corner radius with system_app_widget_background_radius on API 31+ (10dp fallback below), so the widgets match the rest of the home screen. --- .../android/app/src/main/res/drawable/layout_bg.xml | 2 +- .../android/app/src/main/res/layout/new_note_widget.xml | 3 +-- .../android/app/src/main/res/layout/note_widget.xml | 3 +-- .../app/src/main/res/layout/note_widget_preview.xml | 3 +-- .../android/app/src/main/res/values-night/colors.xml | 4 ---- .../mobile/android/app/src/main/res/values-v31/dimens.xml | 8 ++++++++ apps/mobile/android/app/src/main/res/values/attrs.xml | 6 ------ apps/mobile/android/app/src/main/res/values/colors.xml | 4 ---- apps/mobile/android/app/src/main/res/values/dimens.xml | 4 ++++ apps/mobile/android/app/src/main/res/values/themes.xml | 7 ------- 10 files changed, 16 insertions(+), 28 deletions(-) create mode 100644 apps/mobile/android/app/src/main/res/values-v31/dimens.xml delete mode 100644 apps/mobile/android/app/src/main/res/values/attrs.xml delete mode 100644 apps/mobile/android/app/src/main/res/values/themes.xml diff --git a/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml b/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml index 4e9108ff5..af55a9847 100644 --- a/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml +++ b/apps/mobile/android/app/src/main/res/drawable/layout_bg.xml @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml b/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml index 622886bd1..70bb73b1b 100644 --- a/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml +++ b/apps/mobile/android/app/src/main/res/layout/new_note_widget.xml @@ -2,8 +2,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@android:color/transparent" - android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer"> + android:background="@android:color/transparent"> + android:background="@android:color/transparent"> + android:background="@android:color/transparent"> - #FFE1F5FE - #FF81D4FA - #FF039BE5 - #FF01579B #1f1f1f #1D1D1D #2E2E2E diff --git a/apps/mobile/android/app/src/main/res/values-v31/dimens.xml b/apps/mobile/android/app/src/main/res/values-v31/dimens.xml new file mode 100644 index 000000000..c3e87c9e4 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values-v31/dimens.xml @@ -0,0 +1,8 @@ + + + + + @android:dimen/system_app_widget_background_radius + + diff --git a/apps/mobile/android/app/src/main/res/values/attrs.xml b/apps/mobile/android/app/src/main/res/values/attrs.xml deleted file mode 100644 index 97531a256..000000000 --- a/apps/mobile/android/app/src/main/res/values/attrs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/colors.xml b/apps/mobile/android/app/src/main/res/values/colors.xml index 4ac8bc762..ab4d9ecc4 100644 --- a/apps/mobile/android/app/src/main/res/values/colors.xml +++ b/apps/mobile/android/app/src/main/res/values/colors.xml @@ -1,9 +1,5 @@ - #FFE1F5FE - #FF81D4FA - #FF039BE5 - #FF01579B #FFFFFF #DCEDEDED #BFBFBF diff --git a/apps/mobile/android/app/src/main/res/values/dimens.xml b/apps/mobile/android/app/src/main/res/values/dimens.xml index 4db8c5906..7aa2b16bc 100644 --- a/apps/mobile/android/app/src/main/res/values/dimens.xml +++ b/apps/mobile/android/app/src/main/res/values/dimens.xml @@ -7,4 +7,8 @@ http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout --> 0dp + + 10dp + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/values/themes.xml b/apps/mobile/android/app/src/main/res/values/themes.xml deleted file mode 100644 index 935088e01..000000000 --- a/apps/mobile/android/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file From 8d2e1c65bc456068b7234aa22f9eefa36da33bce Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:21:49 +0500 Subject: [PATCH 13/20] mobile: keep note and reminder content off the lock screen From Android 16 QPR1 widgets are lock screen eligible by default, so the note widget (title + preview text) and the reminders widget (titles and descriptions) could render user content on a locked device. Adds res/xml-v36 overrides declaring not_keyguard for both. The new note widget is left eligible - it is only a button and leaks nothing. --- .../src/main/res/xml-v36/note_widget_info.xml | 22 +++++++++++++++++++ .../res/xml-v36/widget_reminders_info.xml | 20 +++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml create mode 100644 apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml diff --git a/apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml b/apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml new file mode 100644 index 000000000..97a47ff1a --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml-v36/note_widget_info.xml @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml new file mode 100644 index 000000000..9bca769e1 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml @@ -0,0 +1,20 @@ + + \ No newline at end of file From b066fac6923374e325dadaf38d21f6cbec0f70fa Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Mon, 27 Jul 2026 12:24:55 +0500 Subject: [PATCH 14/20] mobile: drop unused imports from widget source --- .../streetwriters/notesnook/NotePreviewConfigureActivity.java | 4 ---- .../java/com/streetwriters/notesnook/RCTNNativeModule.java | 1 - 2 files changed, 5 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java index 4418ce56f..3c6035abd 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/NotePreviewConfigureActivity.java @@ -5,15 +5,11 @@ import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.util.Log; -import android.widget.RemoteViews; import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.defaults.DefaultReactActivityDelegate; -import com.google.gson.Gson; -import com.streetwriters.notesnook.datatypes.Note; public class NotePreviewConfigureActivity extends ReactActivity { 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 cf21ecbdf..175a06da6 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 @@ -12,7 +12,6 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.graphics.RectF; import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; From 77e56f9da1f7ec545279e12d6fc0ef6517d06c27 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 29 Jul 2026 09:08:03 +0500 Subject: [PATCH 15/20] mobile: fix widgets do not refresh after app data is cleared, require a restart. --- .../notesnook/RCTNNativeModule.java | 10 ++++ .../streetwriters/notesnook/WidgetUtils.java | 54 +++++++++++++++++++ apps/mobile/app/hooks/use-app-events.tsx | 6 +++ .../app/services/note-preview-widget.ts | 4 ++ apps/mobile/app/utils/notesnook-module.ts | 2 + 5 files changed, 76 insertions(+) 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), From 71cae07dc02c5bcb19e62500b36a92e0490e534e Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 29 Jul 2026 09:13:59 +0500 Subject: [PATCH 16/20] mobile: update widgets on time changed When system time changes, reminder widgets keep showing reminders that have passed as active. --- .../android/app/src/main/AndroidManifest.xml | 9 ++++++ .../notesnook/WidgetTimeChangeReceiver.java | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/WidgetTimeChangeReceiver.java diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 4b6724133..a3267c669 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -96,6 +96,15 @@ android:resource="@xml/note_widget_info" /> + + + + + + + Date: Wed, 29 Jul 2026 09:37:26 +0500 Subject: [PATCH 17/20] mobile: reminder widget does not show up on some devices --- .../src/main/res/xml-v36/widget_reminders_info.xml | 13 ++++--------- .../app/src/main/res/xml/widget_reminders_info.xml | 8 ++++---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml index 9bca769e1..653f52e8f 100644 --- a/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml +++ b/apps/mobile/android/app/src/main/res/xml-v36/widget_reminders_info.xml @@ -1,14 +1,9 @@ - Date: Wed, 29 Jul 2026 10:01:29 +0500 Subject: [PATCH 18/20] mobile: fix reminder widget title vertical padding --- .../android/app/src/main/res/layout/widget_reminders.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml index c7bc790c5..c2b1683e2 100644 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml @@ -12,22 +12,23 @@ android:orientation="horizontal" android:paddingHorizontal="12dp" android:layout_gravity="center" - android:paddingTop="12dp" + android:paddingTop="8dp" + android:paddingBottom="8dp" > From 7f5215403ba6f082704b9bf08ac7daa327b8543f Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 29 Jul 2026 12:14:15 +0500 Subject: [PATCH 19/20] mobile: keep reminders showing in widget after they go off for 3 hours --- .../notesnook/ReminderWidgetProvider.java | 2 +- .../streetwriters/notesnook/WidgetUtils.java | 29 ++++++++++----- apps/mobile/app/services/notifications.ts | 37 +++++++++++-------- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java index 4619f594c..bcbfb0233 100644 --- a/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java +++ b/apps/mobile/android/app/src/main/java/com/streetwriters/notesnook/ReminderWidgetProvider.java @@ -43,7 +43,7 @@ public class ReminderWidgetProvider extends AppWidgetProvider { // The rows travel with the update itself, so there is no bound service to keep in sync and // nothing to invalidate separately: every update redraws from the current data. - List reminders = WidgetUtils.getActiveReminders(context); + List reminders = WidgetUtils.getWidgetReminders(context); RemoteViewsCompat.RemoteCollectionItems.Builder items = new RemoteViewsCompat.RemoteCollectionItems.Builder(); for (Reminder reminder : reminders) { 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 c04ec351c..c23d55c44 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 @@ -25,6 +25,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * Shared helpers for the home screen widgets. @@ -135,10 +136,10 @@ public class WidgetUtils { } /** - * 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. + * The reminders the app last wrote out, minus any that have now dropped out of view. Reading + * and filtering happens here so the provider can push the rows straight into the widget. */ - static List getActiveReminders(Context context) { + static List getWidgetReminders(Context context) { SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); List stored = null; try { @@ -152,7 +153,7 @@ public class WidgetUtils { if (stored == null) return active; for (Reminder reminder : stored) { - if (!isReminderActive(reminder)) continue; + if (!isVisibleInWidget(reminder)) continue; if (active.size() >= MAX_REMINDERS) { Log.w("Reminders", "Widget list truncated to " + MAX_REMINDERS + " reminders"); break; @@ -211,12 +212,20 @@ public class WidgetUtils { } /** - * Whether a reminder should still be listed. Mirrors isReminderActive() in the core package, - * which the app applies when it writes the list out. We re-check here because the stored list - * is only rewritten while the app runs, so one-off reminders would otherwise linger in the - * widget long after they fired. + * How long a reminder keeps its place in the list after going off, so the user can see that it + * happened rather than watching it vanish. Must match RECENTLY_PASSED_WINDOW in + * services/notifications.ts, which decides what gets written out in the first place. */ - static boolean isReminderActive(Reminder reminder) { + private static final long RECENTLY_PASSED_WINDOW_MS = TimeUnit.HOURS.toMillis(3); + + /** + * Whether a reminder should still be drawn. + * + * We re-check here rather than trusting the stored list because that list is only rewritten + * while the app runs. This is what actually retires a reminder once its grace period is up: + * every redraw re-evaluates it against the current time. + */ + static boolean isVisibleInWidget(Reminder reminder) { if (reminder == null) return false; if (reminder.isDisabled()) return false; @@ -225,7 +234,7 @@ public class WidgetUtils { if (!"once".equals(reminder.getMode())) return true; long triggerDate = reminder.getTriggerDate() > 0 ? reminder.getTriggerDate() : reminder.getDate(); - return triggerDate > now; + return triggerDate > now - RECENTLY_PASSED_WINDOW_MS; } /** diff --git a/apps/mobile/app/services/notifications.ts b/apps/mobile/app/services/notifications.ts index 389beca39..6dd620b5c 100644 --- a/apps/mobile/app/services/notifications.ts +++ b/apps/mobile/app/services/notifications.ts @@ -271,6 +271,8 @@ type ReminderWithFormattedTime = Reminder & { formattedDateTime?: string; }; +const RECENTLY_PASSED_WINDOW = 3 * 60 * 60 * 1000; + async function updateRemindersForWidget() { if (Platform.OS === "ios") return; const reminders: ReminderWithFormattedTime[] = await db.reminders?.all.items( @@ -280,28 +282,33 @@ async function updateRemindersForWidget() { sortDirection: "asc" } ); - const activeReminders = []; + const widgetReminders = []; if (!reminders) return; for (const reminder of reminders) { - if (isReminderActive(reminder)) { - const triggerDate = - reminder.snoozeUntil && reminder.snoozeUntil > Date.now() - ? reminder.snoozeUntil - : reminder.mode === "repeat" - ? getUpcomingReminderTime(reminder) - : reminder.date; + const triggerDate = + reminder.snoozeUntil && reminder.snoozeUntil > Date.now() + ? reminder.snoozeUntil + : reminder.mode === "repeat" + ? getUpcomingReminderTime(reminder) + : reminder.date; - reminder.triggerDate = triggerDate; - reminder.formattedTimeOfDay = getFormattedDate(triggerDate, "time"); - reminder.formattedDateTime = getFormattedDate(triggerDate, "date-time"); - reminder.formattedTime = getFormattedReminderTime(reminder); - activeReminders.push(reminder); - } + const recentlyPassed = + reminder.mode === "once" && + !reminder.disabled && + triggerDate > Date.now() - RECENTLY_PASSED_WINDOW; + + if (!isReminderActive(reminder) && !recentlyPassed) continue; + + reminder.triggerDate = triggerDate; + reminder.formattedTimeOfDay = getFormattedDate(triggerDate, "time"); + reminder.formattedDateTime = getFormattedDate(triggerDate, "date-time"); + reminder.formattedTime = getFormattedReminderTime(reminder); + widgetReminders.push(reminder); } NotesnookModule.setString( "appPreview", "remindersList", - JSON.stringify(activeReminders) + JSON.stringify(widgetReminders) ); NotesnookModule.updateReminderWidget(); } From eef56916dee9c0d2ab63a0a0e232406575d042b1 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 30 Jul 2026 09:40:43 +0500 Subject: [PATCH 20/20] mobile: Upcoming Reminders -> Reminders in home widgets --- .../mobile/android/app/src/main/res/layout/widget_reminders.xml | 2 +- .../app/src/main/res/layout/widget_reminders_preview.xml | 2 +- apps/mobile/android/app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml index c2b1683e2..10b723840 100644 --- a/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml +++ b/apps/mobile/android/app/src/main/res/layout/widget_reminders.xml @@ -23,7 +23,7 @@ android:textSize="15sp" android:textStyle="bold" android:textColor="@color/text" - android:text="Upcoming Reminders"/> + android:text="Reminders"/> diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml index 51cd67101..d5b719bcd 100644 --- a/apps/mobile/android/app/src/main/res/values/strings.xml +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -4,7 +4,7 @@ EXAMPLE Add widget Take a quick note. - Quick overview of upcoming reminders + Quick overview of reminders Reminders Note Add a note to home screen