11package com .termux .widget ;
22
3+ import android .annotation .SuppressLint ;
34import android .app .PendingIntent ;
45import android .appwidget .AppWidgetManager ;
56import android .appwidget .AppWidgetProvider ;
7+ import android .content .ComponentName ;
68import android .content .Context ;
79import android .content .Intent ;
810import android .net .Uri ;
1113import android .widget .RemoteViews ;
1214import android .widget .Toast ;
1315
16+ import androidx .annotation .NonNull ;
17+
1418import com .google .common .base .Joiner ;
1519import com .termux .shared .data .DataUtils ;
1620import com .termux .shared .data .IntentUtils ;
3034import com .termux .widget .utils .ShortcutUtils ;
3135
3236import 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.
0 commit comments