Skip to content

Commit 497c1cf

Browse files
Added|Fixed: Add support for refreshing all widgets in in-app button and passing 0 in ACTION_REFRESH_WIDGET broadcast
This also allows a way to fix occasional non-responsive widgets after app updates if `ACTION_APPWIDGET_UPDATE` was not sent/received by Termux:Widget app. This commit also adds logging to widget callbacks for debugging issues.
1 parent 03c9cb7 commit 497c1cf

File tree

7 files changed

+223
-57
lines changed

7 files changed

+223
-57
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ chmod 700 -R /data/data/com.termux/files/home/.shortcuts/tasks
8484

8585
Once you have created the directories, you can then create scripts files as per instructions in [Creating And Modifying Scripts](#Creating-And-Modifying-Scripts).
8686

87-
Once you have created script files, you can add a launcher widget for the `Termux:Widget` app that will show the list of the script files, which you can execute by clicking them. If you create/modify shortcuts files, you will have to press the refresh button on the widget for the updated list to be shown. You can also refresh a specific widget by running `am broadcast -n com.termux.widget/.TermuxWidgetProvider -a com.termux.widget.ACTION_REFRESH_WIDGET --ei appWidgetId <id>` from Termux terminal/scripts for version `>= 0.13.0`, where `id` is the number in the `Termux shortcuts reloaded (<id>)` flash shown when you press the refresh button.
87+
Once you have created script files, you can add a launcher widget for the `Termux:Widget` app that will show the list of the script files, which you can execute by clicking them. If you create/modify shortcuts files, you will have to press the refresh button on the widget for the updated list to be shown. You can also update all widgets from inside the app with the `REFRESH` button in the refresh widgets section. You can also refresh a specific widget by running `am broadcast -n com.termux.widget/.TermuxWidgetProvider -a com.termux.widget.ACTION_REFRESH_WIDGET --ei appWidgetId <id>` from Termux terminal/scripts for version `>= 0.13.0`, where `id` is the number in the `Termux widgets reloaded: <id>)` flash shown when you press the refresh button. You can pass `0` to update all widgets for version `>= 0.114.0`. Refreshing widgets with the in-app `REFRESH` button or running command with id `0` may also be needed in some cases after app updates where widgets become non-responsive and do not show any shortcuts and refresh buttons of the widgets itself do not work either.
8888

8989
You can also add a launcher shortcut or dynamic shortcut for any script file with an optional custom icon as detailed in [Script Icon Directory](#script-icon-directory-optional).
9090

app/src/main/java/com/termux/widget/TermuxWidgetProvider.java

Lines changed: 148 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.termux.widget;
22

3+
import android.annotation.SuppressLint;
34
import android.app.PendingIntent;
45
import android.appwidget.AppWidgetManager;
56
import android.appwidget.AppWidgetProvider;
7+
import android.content.ComponentName;
68
import android.content.Context;
79
import android.content.Intent;
810
import android.net.Uri;
@@ -11,6 +13,8 @@
1113
import android.widget.RemoteViews;
1214
import android.widget.Toast;
1315

16+
import androidx.annotation.NonNull;
17+
1418
import com.google.common.base.Joiner;
1519
import com.termux.shared.data.DataUtils;
1620
import com.termux.shared.data.IntentUtils;
@@ -30,6 +34,9 @@
3034
import com.termux.widget.utils.ShortcutUtils;
3135

3236
import java.io.File;
37+
import java.util.ArrayList;
38+
import java.util.Arrays;
39+
import java.util.List;
3340

3441
/**
3542
* Widget providing a list to launch scripts in ~/.shortcuts/.
@@ -41,6 +48,8 @@ public final class TermuxWidgetProvider extends AppWidgetProvider {
4148
private static final String LOG_TAG = "TermuxWidgetProvider";
4249

4350
public void onEnabled(Context context) {
51+
Logger.logDebug(LOG_TAG, "onEnabled");
52+
4453
String errmsg = TermuxUtils.isTermuxAppAccessible(context);
4554
if (errmsg != null) {
4655
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
@@ -58,76 +67,165 @@ public void onEnabled(Context context) {
5867
*/
5968
@Override
6069
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
70+
super.onUpdate(context, appWidgetManager, appWidgetIds);
71+
72+
Logger.logDebug(LOG_TAG, "onUpdate: " + Arrays.toString(appWidgetIds));
73+
if (appWidgetIds == null || appWidgetIds.length == 0) return;
74+
6175
for (int appWidgetId : appWidgetIds) {
62-
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
63-
64-
// The empty view is displayed when the collection has no items. It should be a sibling
65-
// of the collection view:
66-
rv.setEmptyView(R.id.widget_list, R.id.empty_view);
67-
68-
// Setup intent which points to the TermuxWidgetService which will provide the views for this collection.
69-
Intent intent = new Intent(context, TermuxWidgetService.class);
70-
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
71-
// When intents are compared, the extras are ignored, so we need to embed the extras
72-
// into the data so that the extras will not be ignored.
73-
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
74-
rv.setRemoteAdapter(R.id.widget_list, intent);
75-
76-
// Setup refresh button:
77-
Intent refreshIntent = new Intent(context, TermuxWidgetProvider.class);
78-
refreshIntent.setAction(TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET);
79-
refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
80-
refreshIntent.setData(Uri.parse(refreshIntent.toUri(Intent.URI_INTENT_SCHEME)));
81-
PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(context, 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT);
82-
rv.setOnClickPendingIntent(R.id.refresh_button, refreshPendingIntent);
83-
84-
// Here we setup the a pending intent template. Individuals items of a collection
85-
// cannot setup their own pending intents, instead, the collection as a whole can
86-
// setup a pending intent template, and the individual items can set a fillInIntent
87-
// to create unique before on an item to item basis.
88-
Intent toastIntent = new Intent(context, TermuxWidgetProvider.class);
89-
toastIntent.setAction(TERMUX_WIDGET_PROVIDER.ACTION_WIDGET_ITEM_CLICKED);
90-
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
91-
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
92-
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
93-
rv.setPendingIntentTemplate(R.id.widget_list, toastPendingIntent);
94-
95-
appWidgetManager.updateAppWidget(appWidgetId, rv);
76+
updateAppWidgetRemoteViews(context, appWidgetManager, appWidgetId);
9677
}
9778
}
9879

80+
public static void updateAppWidgetRemoteViews(@NonNull Context context, @NonNull AppWidgetManager appWidgetManager, int appWidgetId) {
81+
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return;
82+
83+
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
84+
85+
// The empty view is displayed when the collection has no items. It should be a sibling
86+
// of the collection view:
87+
remoteViews.setEmptyView(R.id.widget_list, R.id.empty_view);
88+
89+
// Setup intent which points to the TermuxWidgetService which will provide the views for this collection.
90+
Intent intent = new Intent(context, TermuxWidgetService.class);
91+
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
92+
// When intents are compared, the extras are ignored, so we need to embed the extras
93+
// into the data so that the extras will not be ignored.
94+
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
95+
remoteViews.setRemoteAdapter(R.id.widget_list, intent);
96+
97+
// Setup refresh button:
98+
Intent refreshIntent = new Intent(context, TermuxWidgetProvider.class);
99+
refreshIntent.setAction(TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET);
100+
refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
101+
refreshIntent.setData(Uri.parse(refreshIntent.toUri(Intent.URI_INTENT_SCHEME)));
102+
@SuppressLint("UnspecifiedImmutableFlag") // Must be mutable
103+
PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(context, 0, refreshIntent,
104+
PendingIntent.FLAG_UPDATE_CURRENT);
105+
remoteViews.setOnClickPendingIntent(R.id.refresh_button, refreshPendingIntent);
106+
107+
// Here we setup the a pending intent template. Individuals items of a collection
108+
// cannot setup their own pending intents, instead, the collection as a whole can
109+
// setup a pending intent template, and the individual items can set a fillInIntent
110+
// to create unique before on an item to item basis.
111+
Intent toastIntent = new Intent(context, TermuxWidgetProvider.class);
112+
toastIntent.setAction(TERMUX_WIDGET_PROVIDER.ACTION_WIDGET_ITEM_CLICKED);
113+
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
114+
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
115+
@SuppressLint("UnspecifiedImmutableFlag") // Must be mutable
116+
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
117+
PendingIntent.FLAG_UPDATE_CURRENT);
118+
remoteViews.setPendingIntentTemplate(R.id.widget_list, toastPendingIntent);
119+
120+
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
121+
}
122+
123+
@Override
124+
public void onDeleted(Context context, int[] appWidgetIds) {
125+
Logger.logDebug(LOG_TAG, "onDeleted");
126+
}
127+
128+
@Override
129+
public void onDisabled(Context context) {
130+
Logger.logDebug(LOG_TAG, "onDisabled");
131+
}
132+
99133
@Override
100134
public void onReceive(Context context, Intent intent) {
101-
super.onReceive(context, intent);
135+
String action = intent != null ? intent.getAction() : null;
136+
if (action == null) return;
137+
138+
Logger.logDebug(LOG_TAG, "onReceive(): " + action);
139+
Logger.logVerbose(LOG_TAG, "Intent Received\n" + IntentUtils.getIntentString(intent));
140+
141+
switch (action) {
142+
case AppWidgetManager.ACTION_APPWIDGET_UPDATE: {
143+
// The super class already handles this to call onUpdate to update remove views, but
144+
// we handle this ourselves and call notifyAppWidgetViewDataChanged as well afterwards.
145+
if (!ShortcutUtils.isTermuxAppAccessible(context, LOG_TAG, false)) return;
102146

103-
switch (intent.getAction()) {
104-
case TERMUX_WIDGET_PROVIDER.ACTION_WIDGET_ITEM_CLICKED:
147+
refreshAppWidgets(context, intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS), true);
148+
149+
return;
150+
} case TERMUX_WIDGET_PROVIDER.ACTION_WIDGET_ITEM_CLICKED: {
105151
String clickedFilePath = intent.getStringExtra(TERMUX_WIDGET_PROVIDER.EXTRA_FILE_CLICKED);
106-
if (FileUtils.getFileType(clickedFilePath, true) == FileType.DIRECTORY) return;
107-
sendExecutionIntentToTermuxService(context, clickedFilePath, LOG_TAG);
108-
break;
109-
case TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET:
110-
String errmsg = TermuxUtils.isTermuxAppAccessible(context);
111-
if (errmsg != null) {
112-
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
152+
if (clickedFilePath == null || clickedFilePath.isEmpty()) {
153+
Logger.logError(LOG_TAG, "Ignoring unset clicked file");
113154
return;
114155
}
115156

157+
if (FileUtils.getFileType(clickedFilePath, true) == FileType.DIRECTORY) {
158+
Logger.logError(LOG_TAG, "Ignoring clicked directory file");
159+
return;
160+
}
161+
162+
sendExecutionIntentToTermuxService(context, clickedFilePath, LOG_TAG);
163+
return;
164+
165+
} case TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET: {
166+
if (!ShortcutUtils.isTermuxAppAccessible(context, LOG_TAG, true)) return;
167+
116168
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
117-
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return;
118-
AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
169+
int[] appWidgetIds;
170+
boolean updateRemoteViews = false;
171+
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
172+
appWidgetIds = new int[]{appWidgetId};
173+
} else {
174+
appWidgetIds = AppWidgetManager.getInstance(context).getAppWidgetIds(new ComponentName(context, TermuxWidgetProvider.class));
175+
Logger.logDebug(LOG_TAG, "Refreshing all widget ids: " + Arrays.toString(appWidgetIds));
176+
177+
// Only update remote views if sendIntentToRefreshAllWidgets() is called or if
178+
// user sent intent with "am broadcast" command.
179+
// A valid id would normally only be sent if refresh button of widget was successfully
180+
// pressed and widget was not in a non-responsive state, so no need to update remote views.
181+
updateRemoteViews = true;
182+
}
119183

120-
Toast toast = Toast.makeText(context, context.getString(R.string.msg_scripts_reloaded, appWidgetId), Toast.LENGTH_SHORT);
121-
toast.setGravity(Gravity.CENTER, 0, 0);
122-
toast.show();
184+
List<Integer> updatedAppWidgetIds = refreshAppWidgets(context, appWidgetIds, updateRemoteViews);
185+
if (updatedAppWidgetIds != null)
186+
Logger.logDebugAndShowToast(context, LOG_TAG, context.getString(R.string.msg_widgets_reloaded, Arrays.toString(appWidgetIds)));
187+
else
188+
Logger.logDebugAndShowToast(context, LOG_TAG, context.getString(R.string.msg_no_widgets_found_to_reload));
189+
return;
190+
191+
} default: {
192+
Logger.logDebug(LOG_TAG, "Unhandled action: " + action);
123193
break;
194+
195+
}
124196
}
125-
}
126197

198+
// Allow super to handle other actions
199+
super.onReceive(context, intent);
200+
}
127201

202+
public static List<Integer> refreshAppWidgets(@NonNull Context context, int[] appWidgetIds, boolean updateRemoteViews) {
203+
if (appWidgetIds == null || appWidgetIds.length == 0) return null;
204+
List<Integer> updatedAppWidgetIds = new ArrayList<>();
205+
for (int appWidgetId : appWidgetIds) {
206+
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) continue;
207+
updatedAppWidgetIds.add(appWidgetId);
208+
if (updateRemoteViews)
209+
updateAppWidgetRemoteViews(context, AppWidgetManager.getInstance(context), appWidgetId);
128210

211+
AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
212+
}
129213

214+
return updatedAppWidgetIds.size() > 0 ? updatedAppWidgetIds : null;
215+
}
130216

217+
public static void sendIntentToRefreshAllWidgets(@NonNull Context context, @NonNull String logTag) {
218+
Intent intent = new Intent(TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET);
219+
intent.setClass(context, TermuxWidgetProvider.class);
220+
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
221+
try {
222+
Logger.logDebug(logTag, "Sending intent to refresh all widgets");
223+
context.sendBroadcast(intent);
224+
} catch (Exception e) {
225+
Logger.showToast(context, e.getMessage(), true);
226+
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to send intent to refresh all widgets", e);
227+
}
228+
}
131229

132230
/**
133231
* Extract termux shortcut file path from an intent and send intent to TermuxService to execute it.

app/src/main/java/com/termux/widget/TermuxWidgetService.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.termux.widget;
22

3+
import android.appwidget.AppWidgetManager;
34
import android.content.Context;
45
import android.content.Intent;
56
import android.widget.RemoteViews;
67
import android.widget.RemoteViewsService;
78

9+
import com.termux.shared.data.IntentUtils;
10+
import com.termux.shared.logger.Logger;
811
import com.termux.shared.termux.TermuxConstants;
912
import com.termux.widget.utils.ShortcutUtils;
1013

@@ -13,28 +16,36 @@
1316

1417
public final class TermuxWidgetService extends RemoteViewsService {
1518

19+
private static final String LOG_TAG = "TermuxWidgetService";
20+
1621
@Override
1722
public RemoteViewsFactory onGetViewFactory(Intent intent) {
18-
return new ListRemoteViewsFactory(getApplicationContext());
23+
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
24+
Logger.logDebug(LOG_TAG, "onGetViewFactory(): " + appWidgetId);
25+
Logger.logVerbose(LOG_TAG, "Intent Received\n" + IntentUtils.getIntentString(intent));
26+
27+
return new ListRemoteViewsFactory(getApplicationContext(), appWidgetId);
1928
}
2029

2130
public static class ListRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
31+
2232
private final List<ShortcutFile> shortcutFiles = new ArrayList<>();
2333
private final Context mContext;
34+
private final int mAppWidgetId;
2435

25-
public ListRemoteViewsFactory(Context context) {
36+
public ListRemoteViewsFactory(Context context, int appWidgetId) {
2637
mContext = context;
38+
mAppWidgetId = appWidgetId;
2739
}
2840

2941
@Override
3042
public void onCreate() {
31-
// In onCreate() you setup any connections / cursors to your data source. Heavy lifting,
32-
// for example downloading or creating content etc, should be deferred to onDataSetChanged()
33-
// or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
43+
Logger.logDebug(LOG_TAG, "onCreate(): " + mAppWidgetId);
3444
}
3545

3646
@Override
3747
public void onDestroy() {
48+
Logger.logDebug(LOG_TAG, "onDestroy(): " + mAppWidgetId);
3849
shortcutFiles.clear();
3950
}
4051

@@ -73,6 +84,8 @@ public boolean hasStableIds() {
7384

7485
@Override
7586
public void onDataSetChanged() {
87+
Logger.logDebug(LOG_TAG, "onDataSetChanged(): " + mAppWidgetId);
88+
7689
// This is triggered when you call AppWidgetManager notifyAppWidgetViewDataChanged
7790
// on the collection view corresponding to this factory. You can do heaving lifting in
7891
// here, synchronously. For example, if you need to process an image, fetch something

app/src/main/java/com/termux/widget/activities/TermuxWidgetActivity.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.termux.shared.termux.TermuxConstants;
2323
import com.termux.widget.R;
2424
import com.termux.widget.ShortcutFile;
25+
import com.termux.widget.TermuxWidgetProvider;
2526
import com.termux.widget.utils.ShortcutUtils;
2627

2728
import java.io.File;
@@ -55,6 +56,7 @@ protected void onCreate(Bundle savedInstanceState) {
5556

5657
setDisableLauncherIconViews();
5758
setDynamicShortcutsViews();
59+
setRefreshAllWidgetsViews();
5860
}
5961

6062
@Override
@@ -111,6 +113,11 @@ private void setDynamicShortcutsViews() {
111113
}
112114
}
113115

116+
private void setRefreshAllWidgetsViews() {
117+
Button refreshAllWidgetsIconButton = findViewById(R.id.btn_refresh_all_widgets);
118+
refreshAllWidgetsIconButton.setOnClickListener(v -> TermuxWidgetProvider.sendIntentToRefreshAllWidgets(TermuxWidgetActivity.this, LOG_TAG));
119+
}
120+
114121
@RequiresApi(Build.VERSION_CODES.N_MR1)
115122
private void createDynamicShortcuts(@NonNull Context context) {
116123
ShortcutManager shortcutManager = ShortcutUtils.getShortcutManager(context, LOG_TAG, true);

app/src/main/java/com/termux/widget/utils/ShortcutUtils.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.termux.shared.file.FileUtils;
1111
import com.termux.shared.logger.Logger;
1212
import com.termux.shared.termux.TermuxConstants;
13+
import com.termux.shared.termux.TermuxUtils;
1314
import com.termux.widget.NaturalOrderComparator;
1415
import com.termux.widget.R;
1516
import com.termux.widget.ShortcutFile;
@@ -96,4 +97,13 @@ public static ShortcutManager getShortcutManager(@NonNull Context context, @NonN
9697
return shortcutManager;
9798
}
9899

100+
public static boolean isTermuxAppAccessible(@NonNull Context context, @NonNull String logTag, boolean showErrorToast) {
101+
String errmsg = TermuxUtils.isTermuxAppAccessible(context);
102+
if (errmsg != null) {
103+
Logger.logErrorAndShowToast(showErrorToast ? context : null, logTag, errmsg);
104+
return false;
105+
}
106+
return true;
107+
}
108+
99109
}

0 commit comments

Comments
 (0)