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.