Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create an app widget with a configuration activity, and update it for the first time?

Tags:

This is driving me crazy. I don't know how to update the app widget from the configuration activity, even with the recommended practises. Why the update method is not called on the app widget creation is beyond my understanding.

What I'd like: an app widget containing a collection (with a listview) of items. But the user needs to select something, so I need a configuration activity.

The configuration activity is a ListActivity:

@TargetApi(Build.VERSION_CODES.HONEYCOMB) public class ChecksWidgetConfigureActivity extends SherlockListActivity {     private List<Long> mRowIDs;     int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;     private BaseAdapter mAdapter;      @Override     protected void onCreate(final Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setResult(RESULT_CANCELED);         setContentView(R.layout.checks_widget_configure);          final Intent intent = getIntent();         final Bundle extras = intent.getExtras();         if (extras != null) {             mAppWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);         }          // If they gave us an intent without the widget id, just bail.         if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {             finish();         }          mRowIDs = new ArrayList<Long>(); // it's actually loaded from an ASyncTask, don't worry about that — it works.         mAdapter = new MyListAdapter((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE));         getListView().setAdapter(mAdapter);     }      private class MyListAdapter extends BaseAdapter {         // not relevant...     }      @Override     protected void onListItemClick(final ListView l, final View v, final int position, final long id) {         if (position < mRowIDs.size()) {             // Set widget result             final Intent resultValue = new Intent();             resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);             resultValue.putExtra("rowId", mRowIDs.get(position));             setResult(RESULT_OK, resultValue);              // Request widget update             final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);             ChecksWidgetProvider.updateAppWidget(this, appWidgetManager, mAppWidgetId, mRowIDs);         }          finish();     } } 

As you can see I'm calling a static method from my app widget provider. I got that idea from the official doc.

Let's have a look at my provider:

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public class ChecksWidgetProvider extends AppWidgetProvider {     public static final String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";     public static final String EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM";      @Override     public void onUpdate(final Context context, final AppWidgetManager appWidgetManager, final int[] appWidgetIds) {         super.onUpdate(context, appWidgetManager, appWidgetIds);         final int N = appWidgetIds.length;          // Perform this loop procedure for each App Widget that belongs to this provider         for (int i = 0; i < N; i++) {             // Here we setup the intent which points to the StackViewService which will             // provide the views for this collection.             final Intent intent = new Intent(context, ChecksWidgetService.class);             intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);             // 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)));             final RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.checks_widget);             rv.setRemoteAdapter(android.R.id.list, intent);              // The empty view is displayed when the collection has no items. It should be a sibling             // of the collection view.             rv.setEmptyView(android.R.id.list, android.R.id.empty);              // 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.             final Intent toastIntent = new Intent(context, ChecksWidgetProvider.class);             toastIntent.setAction(ChecksWidgetProvider.TOAST_ACTION);             toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);             toastIntent.setData(Uri.parse(toastIntent.toUri(Intent.URI_INTENT_SCHEME)));             final PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);             rv.setPendingIntentTemplate(android.R.id.list, toastPendingIntent);              appWidgetManager.updateAppWidget(appWidgetIds[i], rv);         }     }      @Override     public void onReceive(final Context context, final Intent intent) {         final AppWidgetManager mgr = AppWidgetManager.getInstance(context);         if (intent.getAction().equals(TOAST_ACTION)) {             final int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);             final long rowId = intent.getLongExtra("rowId", 0);             final int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);             Toast.makeText(context, "Touched view " + viewIndex + " (rowId: " + rowId + ")", Toast.LENGTH_SHORT).show();         }         super.onReceive(context, intent);     }      @Override     public void onAppWidgetOptionsChanged(final Context context, final AppWidgetManager appWidgetManager, final int appWidgetId, final Bundle newOptions) {         updateAppWidget(context, appWidgetManager, appWidgetId, newOptions.getLong("rowId"));     }      public static void updateAppWidget(final Context context, final AppWidgetManager appWidgetManager, final int appWidgetId, final long rowId) {         final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.checks_widget);         appWidgetManager.updateAppWidget(appWidgetId, views);     } } 

This is basically a copy/paste from the official doc. We can see my static method here. Let's pretend that it actually uses the rowId for now.

We can also see another failed (see below) attempt to update the app widget when I receive the options changed broadcast (onAppWidgetOptionsChanged).

The Service required for an app widget based on collections is almost an exact copy/paste of the doc:

@TargetApi(Build.VERSION_CODES.HONEYCOMB) public class ChecksWidgetService extends RemoteViewsService {     @Override     public RemoteViewsFactory onGetViewFactory(final Intent intent) {         return new StackRemoteViewsFactory(this.getApplicationContext(), intent);     } }  class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {     private static final int mCount = 10;     private final List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();     private final Context mContext;     private final int mAppWidgetId;     private final long mRowId;      public StackRemoteViewsFactory(final Context context, final Intent intent) {         mContext = context;         mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);         mRowId = intent.getLongExtra("rowId", 0);     }      @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.         for (int i = 0; i < mCount; i++) {             mWidgetItems.add(new WidgetItem(i + " (rowId: " + mRowId + ") !"));         }          // We sleep for 3 seconds here to show how the empty view appears in the interim.         // The empty view is set in the StackWidgetProvider and should be a sibling of the         // collection view.         try {             Thread.sleep(3000);         } catch (final InterruptedException e) {             e.printStackTrace();         }     }      @Override     public void onDestroy() {         // In onDestroy() you should tear down anything that was setup for your data source,         // eg. cursors, connections, etc.         mWidgetItems.clear();     }      @Override     public int getCount() {         return mCount;     }      @Override     public RemoteViews getViewAt(final int position) {         // position will always range from 0 to getCount() - 1.          // We construct a remote views item based on our widget item xml file, and set the         // text based on the position.         final RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);         rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);          // Next, we set a fill-intent which will be used to fill-in the pending intent template         // which is set on the collection view in StackWidgetProvider.         final Bundle extras = new Bundle();         extras.putInt(ChecksWidgetProvider.EXTRA_ITEM, position);         final Intent fillInIntent = new Intent();         fillInIntent.putExtras(extras);         rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);          // You can do heaving lifting in here, synchronously. For example, if you need to         // process an image, fetch something from the network, etc., it is ok to do it here,         // synchronously. A loading view will show up in lieu of the actual contents in the         // interim.         try {             L.d("Loading view " + position);             Thread.sleep(500);         } catch (final InterruptedException e) {             e.printStackTrace();         }          // Return the remote views object.         return rv;     }      @Override     public RemoteViews getLoadingView() {         // You can create a custom loading view (for instance when getViewAt() is slow.) If you         // return null here, you will get the default loading view.         return null;     }      @Override     public int getViewTypeCount() {         return 1;     }      @Override     public long getItemId(final int position) {         return position;     }      @Override     public boolean hasStableIds() {         return true;     }      @Override     public void onDataSetChanged() {         // 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         // from the network, etc., it is ok to do it here, synchronously. The widget will remain         // in its current state while work is being done here, so you don't need to worry about         // locking up the widget.     } } 

And at last, my widget layout:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout     xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@+id/widgetLayout"     android:orientation="vertical"     android:padding="@dimen/widget_margin"     android:layout_width="match_parent"     android:layout_height="match_parent">      <TextView         android:id="@+id/resizeable_widget_title"         style="@style/show_subTitle"         android:padding="2dp"         android:paddingLeft="5dp"         android:textColor="#FFFFFFFF"         android:background="@drawable/background_pink_striked_transparent"         android:text="@string/show_title_key_dates" />      <ListView         android:id="@android:id/list"         android:layout_marginRight="5dp"         android:layout_marginLeft="5dp"         android:background="@color/timeline_month_dark"         android:layout_width="match_parent"         android:layout_height="match_parent" />      <TextView         android:id="@android:id/empty"         android:layout_width="match_parent"         android:layout_height="match_parent"         android:gravity="center"         android:textColor="#ffffff"         android:textStyle="bold"         android:text="@string/empty_view_text"         android:textSize="20sp" />  </LinearLayout> 

The relevant section of my android manifest XML file:

<receiver android:name="com.my.full.pkg.ChecksWidgetProvider">     <intent-filter>             <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />     </intent-filter>      <meta-data             android:name="android.appwidget.provider"             android:resource="@xml/checks_widget_info" /> </receiver> <activity android:name="com.my.full.pkg.ChecksWidgetConfigureActivity">     <intent-filter>             <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />     </intent-filter> </activity> <service     android:name="com.my.full.pkg.ChecksWidgetService"     android:permission="android.permission.BIND_REMOTEVIEWS" /> 

xml/checks_widget_info.xml:

<?xml version="1.0" encoding="utf-8"?> <appwidget-provider     xmlns:android="http://schemas.android.com/apk/res/android"     android:minWidth="146dp"     android:minHeight="146dp"     android:updatePeriodMillis="86400000"     android:initialLayout="@layout/checks_widget"     android:configure="com.my.full.pkg.ChecksWidgetConfigureActivity"     android:resizeMode="horizontal|vertical"     android:previewImage="@drawable/resizeable_widget_preview" /> 

So, what's wrong? Well, when I create the widget it's empty. I mean void. Empty. Nothing. I don't have the empty view defined in my layout! What the hell?

If I reinstall the app or reboot the device (or kill the launcher app), the app widget is actually updated and contains the 10 items that are automatically added as in the example.

I can't get the damn thing to update after the configuration activity finishes. This sentence, taken from the doc, is beyond me: "The onUpdate() method will not be called when the App Widget is created [...]—it is only skipped the first time.".

My question is:

  • Why in the world did the Android dev team chose not to call update for the first time the widget is created?
  • How can I update my app widget before the configuration activity finishes?

Another thing that I don't understand is the action flow:

  1. Install the app with the last code compiled, prepare space on the launcher, open the "widgets" menu from the launcher
  2. Choose my widget and place it to the desired area
  3. At that moment, my app widget provider receives android.appwidget.action.APPWIDGET_ENABLED and then android.appwidget.action.APPWIDGET_UPDATE
  4. Then my app widget provider gets its onUpdate method called. I expected this to happen AFTER the configuration activity finishes...
  5. My configuration activity gets started. But the app widget seems already created AND updated, which I don't understand.
  6. I choose the item from my configuration activity: onListItemClick gets called
  7. The static updateAppWidget from my provider is called, desperately trying to update the widget.
  8. The configuration activity sets its result and finishes.
  9. The provider receives android.appwidget.action.APPWIDGET_UPDATE_OPTIONS: well, that does make a lot of sense to receive a size update when created. That's where I call desperately updateAppWidget
  10. onUpdate from my provider is NOT called. Why??!!

In the end: the widget is empty. Not listview-empty or @android:id/empty-empty, really EMPTY. No view displayed. Nothing.
If I install the app again, the app widget is populated with views inside the listview, as expected.
Resizing the widget has no effect. It just calls onAppWidgetOptionsChanged again, which has no effect.

What I mean by empty: the app widget layout is inflated, but the listview is NOT inflated and the empty view is NOT displayed.

like image 321
Benoit Duffez Avatar asked Dec 04 '12 00:12

Benoit Duffez


People also ask

What is app widget explain with example program?

In android, App widgets are the small application views that can be embedded in other applications such as the Home screen to quickly access the application data. These application views are referred as widgets in the user interface and we can publish these views using an App Widget Provider.


2 Answers

The drawback of doing the update through the AppWidgetManager is that you have to provide the RemoteViews which - from a design point of view - doesn't make sense as the logic related to RemoteViews should be encapsulated within the AppWidgetProvider (or in your case in the RemoteViewsService.RemoteViewsFactory).

SciencyGuy's approach to expose the RemoteViews logic via a static method is one way to deal with that but there's a more elegant solution sending a broadcast directly to the widget:

Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, this, ChecksWidgetProvider.class); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {mAppWidgetId}); sendBroadcast(intent); 

As a consequence the AppWidgetProvider's onUpdate() method will be called to create the RemoteViews for the widget.

like image 106
Emanuel Moecklin Avatar answered Apr 28 '23 23:04

Emanuel Moecklin


You are correct that the onUpdate-method isn't triggered after the configuration activity finishes. It is up to your configuration activity to do the initial update. So you need to build the initial view.

This is the gist of what one should do at the end of the configuration:

// First set result OK with appropriate widgetId Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); setResult(RESULT_OK, resultValue);  // Build/Update widget AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext());  // This is equivalent to your ChecksWidgetProvider.updateAppWidget()     appWidgetManager.updateAppWidget(appWidgetId,                                  ChecksWidgetProvider.buildRemoteViews(getApplicationContext(),                                                                        appWidgetId));  // Updates the collection view, not necessary the first time appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.notes_list);  // Destroy activity finish(); 

You already set the result correctly. And you call ChecksWidgetProvider.updateAppWidget(), however updateAppWidget() does not return the correct result.

updateAppWidget() at current returns an empty RemoteViews-object. Which explains why your widget is completely empty at first. You haven't filled the view with anything. I suggest that you move your code from onUpdate to a static buildRemoteViews() method which you can call from both onUpdate and updateAppWidget():

public static RemoteViews buildRemoteViews(final Context context, final int appWidgetId) {         final RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.checks_widget);         rv.setRemoteAdapter(android.R.id.list, intent);          // The empty view is displayed when the collection has no items. It should be a sibling         // of the collection view.         rv.setEmptyView(android.R.id.list, android.R.id.empty);          // 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.         final Intent toastIntent = new Intent(context, ChecksWidgetProvider.class);         toastIntent.setAction(ChecksWidgetProvider.TOAST_ACTION);         toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);         toastIntent.setData(Uri.parse(toastIntent.toUri(Intent.URI_INTENT_SCHEME)));         final PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);         rv.setPendingIntentTemplate(android.R.id.list, toastPendingIntent);          return rv; }  public static void updateAppWidget(final Context context, final AppWidgetManager appWidgetManager, final int appWidgetId) {     final RemoteViews views = buildRemoteViews(context, appWidgetId);     appWidgetManager.updateAppWidget(appWidgetId, views); }  @Override public void onUpdate(final Context context, final AppWidgetManager appWidgetManager, final int[] appWidgetIds) {     super.onUpdate(context, appWidgetManager, appWidgetIds);      // Perform this loop procedure for each App Widget that belongs to this provider     for (int appWidgetId: appWidgetIds) {         RemoteViews rv = buildRemoteViews(context, appWidgetId);         appWidgetManager.updateAppWidget(appWidgetIds[i], rv);     } } 

That should take care of the widget initialization.

The last step before calling finish() in my sample code is updating the collection view. As the comment says, this isn't necessary the first time. However, I include it just in case you intend to allow a widget to be re-configured after it has been added. In that case, one must update the collection view manually to make sure the appropriate views and data get loaded.

like image 38
Jonas Kalderstam Avatar answered Apr 29 '23 00:04

Jonas Kalderstam