mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
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.
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -231,11 +231,6 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".ReminderViewsService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
||||
@@ -138,7 +138,7 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
if (Objects.equals(extras.getString(IntentType), "NewReminder")) {
|
||||
map.putString(ReminderWidgetProvider.NewReminder, extras.getString(ReminderWidgetProvider.NewReminder));
|
||||
} else if (Objects.equals(extras.getString(IntentType), "OpenReminder")) {
|
||||
map.putString(ReminderViewsService.OpenReminderId, extras.getString(ReminderViewsService.OpenReminderId));
|
||||
map.putString(ReminderWidgetProvider.OpenReminderId, extras.getString(ReminderWidgetProvider.OpenReminderId));
|
||||
} else if (Objects.equals(extras.getString(IntentType), "OpenNote")) {
|
||||
map.putString(NotePreviewWidget.OpenNoteId, extras.getString(NotePreviewWidget.OpenNoteId));
|
||||
}
|
||||
@@ -208,8 +208,8 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
for (int id: ids) {
|
||||
Log.d("Reminders", "Updating" + id);
|
||||
RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.widget_reminders);
|
||||
// The rows are part of this update, so there is nothing left to invalidate afterwards.
|
||||
ReminderWidgetProvider.updateAppWidget(mContext, wm, id, views);
|
||||
wm.notifyAppWidgetViewDataChanged(id, R.id.widget_list_view);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViewsService;
|
||||
import android.content.Context;
|
||||
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.List;
|
||||
|
||||
public class ReminderViewsService extends RemoteViewsService {
|
||||
static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId";
|
||||
@Override
|
||||
public RemoteViewsFactory onGetViewFactory(Intent intent) {
|
||||
return new ReminderRemoteViewsFactory(this.getApplicationContext(), intent);
|
||||
}
|
||||
}
|
||||
|
||||
class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
|
||||
private Context context;
|
||||
private List<Reminder> reminders;
|
||||
|
||||
public ReminderRemoteViewsFactory(Context context, Intent intent) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Initialize reminders list
|
||||
reminders = new ArrayList<Reminder>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataSetChanged() {
|
||||
SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE);
|
||||
List<Reminder> stored = null;
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
stored = gson.fromJson(preferences.getString("remindersList", "[]"), new TypeToken<List<Reminder>>(){}.getType());
|
||||
} catch (Exception e) {
|
||||
Log.e("Reminders", "Could not read the stored reminders list", e);
|
||||
}
|
||||
|
||||
List<Reminder> updated = new ArrayList<Reminder>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<Reminder> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Reminder> getActiveReminders(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
|
||||
List<Reminder> stored = null;
|
||||
try {
|
||||
stored = new Gson().fromJson(preferences.getString(REMINDERS_KEY, "[]"),
|
||||
new TypeToken<List<Reminder>>() {}.getType());
|
||||
} catch (Exception e) {
|
||||
Log.e("Reminders", "Could not read the stored reminders list", e);
|
||||
}
|
||||
|
||||
List<Reminder> 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.
|
||||
|
||||
Reference in New Issue
Block a user