Skip to content

Commit fe38689

Browse files
committed
feat: request widget update from native code
1 parent 47aea3e commit fe38689

5 files changed

Lines changed: 93 additions & 70 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- `packageName` option for defining custom package name for widget
13+
- Ability to request widget update from native code (`RNWidgetJsCommunication#requestWidgetUpdate`)
1314

1415
## [0.16.1] - 2025-04-13
1516

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.reactnativeandroidwidget;
2+
3+
import android.content.Context;
4+
5+
import androidx.work.Data;
6+
import androidx.work.ExistingWorkPolicy;
7+
import androidx.work.OneTimeWorkRequest;
8+
import androidx.work.WorkManager;
9+
10+
import java.util.concurrent.TimeUnit;
11+
12+
public class RNWidgetJsCommunication {
13+
public static void requestWidgetUpdate(Context context, String widgetName) {
14+
int[] widgetIds = RNWidgetUtil.getWidgetIds(context, widgetName);
15+
16+
for (int widgetId : widgetIds) {
17+
Data data = buildData(context, widgetName, widgetId, "WIDGET_UPDATE");
18+
startBackgroundTask(context, data);
19+
}
20+
}
21+
22+
protected static void startBackgroundTask(Context context, Data data) {
23+
workManagerWorkaround(context);
24+
25+
OneTimeWorkRequest headlessJsTaskWorkRequest =
26+
new OneTimeWorkRequest.Builder(RNWidgetBackgroundTaskWorker.class)
27+
.setInputData(data)
28+
.build();
29+
30+
WorkManager
31+
.getInstance(context)
32+
.enqueue(headlessJsTaskWorkRequest);
33+
}
34+
35+
// `APPWIDGET_UPDATE` (`onUpdate`) method is called when the WorkManager queue is empty.
36+
// Since we enqueue only on WorkRequest, the queue will be empty after every execution of the
37+
// widgetTaskHandler.
38+
// This is a bug in android (https://issuetracker.google.com/issues/115575872).
39+
//
40+
// The suggested workaround is to schedule another WorkRequest really far out into the future.
41+
// So, we are creating a OneTimeWorkRequest with an initial delay of 10 years, with a name
42+
// `app.package.WORK_MANAGER_HACK`.
43+
//
44+
// Every time when we schedule another WorkRequest, we REPLACE the WorkRequest with a new one.
45+
// That way there is always at least one outstanding request, and `onUpdate` is not called when
46+
// the WorkManager finishes with its work.
47+
private static void workManagerWorkaround(Context context) {
48+
OneTimeWorkRequest headlessJsTaskWorkRequest =
49+
new OneTimeWorkRequest.Builder(RNWidgetBackgroundTaskWorker.class)
50+
.setInputData(Data.EMPTY)
51+
.setInitialDelay(10 * 365, TimeUnit.DAYS)
52+
.build();
53+
54+
WorkManager.getInstance(context)
55+
.enqueueUniqueWork(
56+
context.getPackageName() + ".WORK_MANAGER_HACK",
57+
ExistingWorkPolicy.REPLACE,
58+
headlessJsTaskWorkRequest
59+
);
60+
}
61+
62+
protected static Data buildData(Context context, String widgetName, int widgetId, String widgetAction, Data additionalData) {
63+
return new Data.Builder()
64+
.putString("widgetName", widgetName)
65+
.putInt("widgetId", widgetId)
66+
.putInt("width", RNWidgetUtil.getWidgetWidth(context, widgetId))
67+
.putInt("height", RNWidgetUtil.getWidgetHeight(context, widgetId))
68+
.putString("widgetAction", widgetAction)
69+
.putAll(additionalData)
70+
.build();
71+
}
72+
73+
protected static Data buildData(Context context, String widgetName, int widgetId, String widgetAction) {
74+
return buildData(context, widgetName, widgetId, widgetAction, Data.EMPTY);
75+
}
76+
}

android/src/main/java/com/reactnativeandroidwidget/RNWidgetProvider.java

Lines changed: 8 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,11 @@
99
import android.os.Bundle;
1010

1111
import androidx.work.Data;
12-
import androidx.work.ExistingWorkPolicy;
13-
import androidx.work.OneTimeWorkRequest;
14-
import androidx.work.WorkManager;
1512

1613
import com.facebook.react.bridge.Arguments;
1714

1815
import org.json.JSONObject;
1916

20-
import java.util.concurrent.TimeUnit;
21-
2217
public class RNWidgetProvider extends AppWidgetProvider {
2318
@Override
2419
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
@@ -27,8 +22,8 @@ public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidge
2722
if (isSizeChanged(context, appWidgetId)) {
2823
storeWidgetSize(context, appWidgetId);
2924

30-
Data data = buildData(context, appWidgetId, "WIDGET_RESIZED");
31-
startBackgroundTask(context, data);
25+
Data data = RNWidgetJsCommunication.buildData(context, getClass().getSimpleName(), appWidgetId, "WIDGET_RESIZED");
26+
RNWidgetJsCommunication.startBackgroundTask(context, data);
3227
}
3328
}
3429

@@ -40,8 +35,8 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a
4035
String widgetAction = isWidgetNew(context, widgetId) ? "WIDGET_ADDED" : "WIDGET_UPDATE";
4136
storeWidgetSize(context, widgetId);
4237

43-
Data data = buildData(context, widgetId, widgetAction);
44-
startBackgroundTask(context, data);
38+
Data data = RNWidgetJsCommunication.buildData(context, getClass().getSimpleName(), widgetId, widgetAction);
39+
RNWidgetJsCommunication.startBackgroundTask(context, data);
4540
}
4641
}
4742

@@ -75,66 +70,11 @@ public void onDeleted(Context context, int[] appWidgetIds) {
7570
removeWidgetSize(context, widgetId);
7671
RNWidgetCollectionService.deleteWidgetImages(context, widgetId);
7772

78-
Data data = buildData(context, widgetId, "WIDGET_DELETED");
79-
startBackgroundTask(context, data);
73+
Data data = RNWidgetJsCommunication.buildData(context, getClass().getSimpleName(), widgetId, "WIDGET_DELETED");
74+
RNWidgetJsCommunication.startBackgroundTask(context, data);
8075
}
8176
}
8277

83-
private Data buildData(Context context, int widgetId, String widgetAction, Data additionalData) {
84-
return new Data.Builder()
85-
.putString("widgetName", getClass().getSimpleName())
86-
.putInt("widgetId", widgetId)
87-
.putInt("width", RNWidgetUtil.getWidgetWidth(context, widgetId))
88-
.putInt("height", RNWidgetUtil.getWidgetHeight(context, widgetId))
89-
.putString("widgetAction", widgetAction)
90-
.putAll(additionalData)
91-
.build();
92-
}
93-
94-
private Data buildData(Context context, int widgetId, String widgetAction) {
95-
return buildData(context, widgetId, widgetAction, Data.EMPTY);
96-
}
97-
98-
private void startBackgroundTask(Context context, Data data) {
99-
workManagerWorkaround(context);
100-
101-
OneTimeWorkRequest headlessJsTaskWorkRequest =
102-
new OneTimeWorkRequest.Builder(RNWidgetBackgroundTaskWorker.class)
103-
.setInputData(data)
104-
.build();
105-
106-
WorkManager
107-
.getInstance(context)
108-
.enqueue(headlessJsTaskWorkRequest);
109-
}
110-
111-
// `APPWIDGET_UPDATE` (`onUpdate`) method is called when the WorkManager queue is empty.
112-
// Since we enqueue only on WorkRequest, the queue will be empty after every execution of the
113-
// widgetTaskHandler.
114-
// This is a bug in android (https://issuetracker.google.com/issues/115575872).
115-
//
116-
// The suggested workaround is to schedule another WorkRequest really far out into the future.
117-
// So, we are creating a OneTimeWorkRequest with an initial delay of 10 years, with a name
118-
// `app.package.WORK_MANAGER_HACK`.
119-
//
120-
// Every time when we schedule another WorkRequest, we REPLACE the WorkRequest with a new one.
121-
// That way there is always at least one outstanding request, and `onUpdate` is not called when
122-
// the WorkManager finishes with its work.
123-
private void workManagerWorkaround(Context context) {
124-
OneTimeWorkRequest headlessJsTaskWorkRequest =
125-
new OneTimeWorkRequest.Builder(RNWidgetBackgroundTaskWorker.class)
126-
.setInputData(Data.EMPTY)
127-
.setInitialDelay(10 * 365, TimeUnit.DAYS)
128-
.build();
129-
130-
WorkManager.getInstance(context)
131-
.enqueueUniqueWork(
132-
context.getPackageName() + ".WORK_MANAGER_HACK",
133-
ExistingWorkPolicy.REPLACE,
134-
headlessJsTaskWorkRequest
135-
);
136-
}
137-
13878
private boolean isSizeChanged(Context context, int widgetId) {
13979
SharedPreferences sharedPref = context.getSharedPreferences(
14080
context.getPackageName() + ".WIDGET_SIZES", Context.MODE_PRIVATE);
@@ -210,7 +150,7 @@ private void handleWidgetClick(Context context, Intent incomingIntent, int widge
210150
.build();
211151
}
212152

213-
Data data = buildData(context, widgetId, "WIDGET_CLICK", additionalData);
214-
startBackgroundTask(context, data);
153+
Data data = RNWidgetJsCommunication.buildData(context, getClass().getSimpleName(), widgetId, "WIDGET_CLICK", additionalData);
154+
RNWidgetJsCommunication.startBackgroundTask(context, data);
215155
}
216156
}

android/src/main/java/com/reactnativeandroidwidget/RNWidgetUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public static WritableMap getScreenInfo(Context context) {
7676
return screenInfo;
7777
}
7878

79-
public static int[] getWidgetIds(ReactApplicationContext context, String widgetName) {
79+
public static int[] getWidgetIds(Context context, String widgetName) {
8080
String widgetProviderClassName = getWidgetProviderClassName(context, widgetName);
8181

8282
if (widgetProviderClassName == null) {

docs/docs/update-widget.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ sidebar_label: Update Widget
55

66
# Update Widget
77

8-
There are two ways to update a widget once it is added on the home screen.
8+
There are three ways to update a widget once it is added on the home screen.
99

1010
## updatePeriodMillis
1111

@@ -37,3 +37,9 @@ More details about `updatePeriodMillis` on the [official documentation](https://
3737
## requestWidgetUpdate
3838

3939
You can call [`requestWidgetUpdate`](./api/request-widget-update.md) any time when your app is open as a result of some user action, and request a widget update.
40+
41+
## Native RNWidgetJsCommunication#requestWidgetUpdate
42+
43+
You can request a widget update from native code (using BroadcastReceiver, AlarmManager, received push notification...).
44+
45+
You can call `com.reactnativeandroidwidget.RNWidgetJsCommunication#requestWidgetUpdate` static method with a context and the name of the widget you want to update, and it will call the javascript `widgetTaskHandler` function with `widgetAction = 'WIDGET_UPDATE'`

0 commit comments

Comments
 (0)