Skip to content

Commit

Permalink
Added|Fixed: Add support for refreshing all widgets in in-app button …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
agnostic-apollo committed Oct 30, 2022
1 parent 03c9cb7 commit 497c1cf
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 57 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ chmod 700 -R /data/data/com.termux/files/home/.shortcuts/tasks

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

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.
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.

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).

Expand Down
198 changes: 148 additions & 50 deletions app/src/main/java/com/termux/widget/TermuxWidgetProvider.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.termux.widget;

import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
Expand All @@ -11,6 +13,8 @@
import android.widget.RemoteViews;
import android.widget.Toast;

import androidx.annotation.NonNull;

import com.google.common.base.Joiner;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
Expand All @@ -30,6 +34,9 @@
import com.termux.widget.utils.ShortcutUtils;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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

public void onEnabled(Context context) {
Logger.logDebug(LOG_TAG, "onEnabled");

String errmsg = TermuxUtils.isTermuxAppAccessible(context);
if (errmsg != null) {
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
Expand All @@ -58,76 +67,165 @@ public void onEnabled(Context context) {
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);

Logger.logDebug(LOG_TAG, "onUpdate: " + Arrays.toString(appWidgetIds));
if (appWidgetIds == null || appWidgetIds.length == 0) return;

for (int appWidgetId : appWidgetIds) {
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);

// The empty view is displayed when the collection has no items. It should be a sibling
// of the collection view:
rv.setEmptyView(R.id.widget_list, R.id.empty_view);

// Setup intent which points to the TermuxWidgetService which will provide the views for this collection.
Intent intent = new Intent(context, TermuxWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
// When intents are compared, the extras are ignored, so we need to embed the extras
// into the data so that the extras will not be ignored.
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
rv.setRemoteAdapter(R.id.widget_list, intent);

// Setup refresh button:
Intent refreshIntent = new Intent(context, TermuxWidgetProvider.class);
refreshIntent.setAction(TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET);
refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
refreshIntent.setData(Uri.parse(refreshIntent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(context, 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.refresh_button, refreshPendingIntent);

// Here we setup the a pending intent template. Individuals items of a collection
// cannot setup their own pending intents, instead, the collection as a whole can
// setup a pending intent template, and the individual items can set a fillInIntent
// to create unique before on an item to item basis.
Intent toastIntent = new Intent(context, TermuxWidgetProvider.class);
toastIntent.setAction(TERMUX_WIDGET_PROVIDER.ACTION_WIDGET_ITEM_CLICKED);
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.widget_list, toastPendingIntent);

appWidgetManager.updateAppWidget(appWidgetId, rv);
updateAppWidgetRemoteViews(context, appWidgetManager, appWidgetId);
}
}

public static void updateAppWidgetRemoteViews(@NonNull Context context, @NonNull AppWidgetManager appWidgetManager, int appWidgetId) {
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return;

RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);

// The empty view is displayed when the collection has no items. It should be a sibling
// of the collection view:
remoteViews.setEmptyView(R.id.widget_list, R.id.empty_view);

// Setup intent which points to the TermuxWidgetService which will provide the views for this collection.
Intent intent = new Intent(context, TermuxWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
// When intents are compared, the extras are ignored, so we need to embed the extras
// into the data so that the extras will not be ignored.
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
remoteViews.setRemoteAdapter(R.id.widget_list, intent);

// Setup refresh button:
Intent refreshIntent = new Intent(context, TermuxWidgetProvider.class);
refreshIntent.setAction(TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET);
refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
refreshIntent.setData(Uri.parse(refreshIntent.toUri(Intent.URI_INTENT_SCHEME)));
@SuppressLint("UnspecifiedImmutableFlag") // Must be mutable
PendingIntent refreshPendingIntent = PendingIntent.getBroadcast(context, 0, refreshIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.refresh_button, refreshPendingIntent);

// Here we setup the a pending intent template. Individuals items of a collection
// cannot setup their own pending intents, instead, the collection as a whole can
// setup a pending intent template, and the individual items can set a fillInIntent
// to create unique before on an item to item basis.
Intent toastIntent = new Intent(context, TermuxWidgetProvider.class);
toastIntent.setAction(TERMUX_WIDGET_PROVIDER.ACTION_WIDGET_ITEM_CLICKED);
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
@SuppressLint("UnspecifiedImmutableFlag") // Must be mutable
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setPendingIntentTemplate(R.id.widget_list, toastPendingIntent);

appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
}

@Override
public void onDeleted(Context context, int[] appWidgetIds) {
Logger.logDebug(LOG_TAG, "onDeleted");
}

@Override
public void onDisabled(Context context) {
Logger.logDebug(LOG_TAG, "onDisabled");
}

@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
String action = intent != null ? intent.getAction() : null;
if (action == null) return;

Logger.logDebug(LOG_TAG, "onReceive(): " + action);
Logger.logVerbose(LOG_TAG, "Intent Received\n" + IntentUtils.getIntentString(intent));

switch (action) {
case AppWidgetManager.ACTION_APPWIDGET_UPDATE: {
// The super class already handles this to call onUpdate to update remove views, but
// we handle this ourselves and call notifyAppWidgetViewDataChanged as well afterwards.
if (!ShortcutUtils.isTermuxAppAccessible(context, LOG_TAG, false)) return;

switch (intent.getAction()) {
case TERMUX_WIDGET_PROVIDER.ACTION_WIDGET_ITEM_CLICKED:
refreshAppWidgets(context, intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS), true);

return;
} case TERMUX_WIDGET_PROVIDER.ACTION_WIDGET_ITEM_CLICKED: {
String clickedFilePath = intent.getStringExtra(TERMUX_WIDGET_PROVIDER.EXTRA_FILE_CLICKED);
if (FileUtils.getFileType(clickedFilePath, true) == FileType.DIRECTORY) return;
sendExecutionIntentToTermuxService(context, clickedFilePath, LOG_TAG);
break;
case TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET:
String errmsg = TermuxUtils.isTermuxAppAccessible(context);
if (errmsg != null) {
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
if (clickedFilePath == null || clickedFilePath.isEmpty()) {
Logger.logError(LOG_TAG, "Ignoring unset clicked file");
return;
}

if (FileUtils.getFileType(clickedFilePath, true) == FileType.DIRECTORY) {
Logger.logError(LOG_TAG, "Ignoring clicked directory file");
return;
}

sendExecutionIntentToTermuxService(context, clickedFilePath, LOG_TAG);
return;

} case TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET: {
if (!ShortcutUtils.isTermuxAppAccessible(context, LOG_TAG, true)) return;

int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return;
AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
int[] appWidgetIds;
boolean updateRemoteViews = false;
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
appWidgetIds = new int[]{appWidgetId};
} else {
appWidgetIds = AppWidgetManager.getInstance(context).getAppWidgetIds(new ComponentName(context, TermuxWidgetProvider.class));
Logger.logDebug(LOG_TAG, "Refreshing all widget ids: " + Arrays.toString(appWidgetIds));

// Only update remote views if sendIntentToRefreshAllWidgets() is called or if
// user sent intent with "am broadcast" command.
// A valid id would normally only be sent if refresh button of widget was successfully
// pressed and widget was not in a non-responsive state, so no need to update remote views.
updateRemoteViews = true;
}

Toast toast = Toast.makeText(context, context.getString(R.string.msg_scripts_reloaded, appWidgetId), Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
List<Integer> updatedAppWidgetIds = refreshAppWidgets(context, appWidgetIds, updateRemoteViews);
if (updatedAppWidgetIds != null)
Logger.logDebugAndShowToast(context, LOG_TAG, context.getString(R.string.msg_widgets_reloaded, Arrays.toString(appWidgetIds)));
else
Logger.logDebugAndShowToast(context, LOG_TAG, context.getString(R.string.msg_no_widgets_found_to_reload));
return;

} default: {
Logger.logDebug(LOG_TAG, "Unhandled action: " + action);
break;

}
}
}

// Allow super to handle other actions
super.onReceive(context, intent);
}

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

AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
}

return updatedAppWidgetIds.size() > 0 ? updatedAppWidgetIds : null;
}

public static void sendIntentToRefreshAllWidgets(@NonNull Context context, @NonNull String logTag) {
Intent intent = new Intent(TERMUX_WIDGET_PROVIDER.ACTION_REFRESH_WIDGET);
intent.setClass(context, TermuxWidgetProvider.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
try {
Logger.logDebug(logTag, "Sending intent to refresh all widgets");
context.sendBroadcast(intent);
} catch (Exception e) {
Logger.showToast(context, e.getMessage(), true);
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to send intent to refresh all widgets", e);
}
}

/**
* Extract termux shortcut file path from an intent and send intent to TermuxService to execute it.
Expand Down
23 changes: 18 additions & 5 deletions app/src/main/java/com/termux/widget/TermuxWidgetService.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.termux.widget;

import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;

import com.termux.shared.data.IntentUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants;
import com.termux.widget.utils.ShortcutUtils;

Expand All @@ -13,28 +16,36 @@

public final class TermuxWidgetService extends RemoteViewsService {

private static final String LOG_TAG = "TermuxWidgetService";

@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new ListRemoteViewsFactory(getApplicationContext());
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
Logger.logDebug(LOG_TAG, "onGetViewFactory(): " + appWidgetId);
Logger.logVerbose(LOG_TAG, "Intent Received\n" + IntentUtils.getIntentString(intent));

return new ListRemoteViewsFactory(getApplicationContext(), appWidgetId);
}

public static class ListRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

private final List<ShortcutFile> shortcutFiles = new ArrayList<>();
private final Context mContext;
private final int mAppWidgetId;

public ListRemoteViewsFactory(Context context) {
public ListRemoteViewsFactory(Context context, int appWidgetId) {
mContext = context;
mAppWidgetId = appWidgetId;
}

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

@Override
public void onDestroy() {
Logger.logDebug(LOG_TAG, "onDestroy(): " + mAppWidgetId);
shortcutFiles.clear();
}

Expand Down Expand Up @@ -73,6 +84,8 @@ public boolean hasStableIds() {

@Override
public void onDataSetChanged() {
Logger.logDebug(LOG_TAG, "onDataSetChanged(): " + mAppWidgetId);

// This is triggered when you call AppWidgetManager notifyAppWidgetViewDataChanged
// on the collection view corresponding to this factory. You can do heaving lifting in
// here, synchronously. For example, if you need to process an image, fetch something
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.termux.shared.termux.TermuxConstants;
import com.termux.widget.R;
import com.termux.widget.ShortcutFile;
import com.termux.widget.TermuxWidgetProvider;
import com.termux.widget.utils.ShortcutUtils;

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

setDisableLauncherIconViews();
setDynamicShortcutsViews();
setRefreshAllWidgetsViews();
}

@Override
Expand Down Expand Up @@ -111,6 +113,11 @@ private void setDynamicShortcutsViews() {
}
}

private void setRefreshAllWidgetsViews() {
Button refreshAllWidgetsIconButton = findViewById(R.id.btn_refresh_all_widgets);
refreshAllWidgetsIconButton.setOnClickListener(v -> TermuxWidgetProvider.sendIntentToRefreshAllWidgets(TermuxWidgetActivity.this, LOG_TAG));
}

@RequiresApi(Build.VERSION_CODES.N_MR1)
private void createDynamicShortcuts(@NonNull Context context) {
ShortcutManager shortcutManager = ShortcutUtils.getShortcutManager(context, LOG_TAG, true);
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/termux/widget/utils/ShortcutUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import com.termux.widget.NaturalOrderComparator;
import com.termux.widget.R;
import com.termux.widget.ShortcutFile;
Expand Down Expand Up @@ -96,4 +97,13 @@ public static ShortcutManager getShortcutManager(@NonNull Context context, @NonN
return shortcutManager;
}

public static boolean isTermuxAppAccessible(@NonNull Context context, @NonNull String logTag, boolean showErrorToast) {
String errmsg = TermuxUtils.isTermuxAppAccessible(context);
if (errmsg != null) {
Logger.logErrorAndShowToast(showErrorToast ? context : null, logTag, errmsg);
return false;
}
return true;
}

}
Loading

0 comments on commit 497c1cf

Please sign in to comment.