Mokkapps
Mokkapps

Reputation: 2028

Android: My App Widget with ListView is not updating via button or update period

I am using a widget to show a RSS feed. The feed is shown but I cannot update it via a button click. The list is also not updated after the update period set in the widget configuration xml.

Can you please help me?

WidgetProvider

public class WidgetProvider extends AppWidgetProvider {

    //Tag for Logging
    private static final String TAG = "Widget";

    // String to be sent on Broadcast as soon as Data is Fetched
    // should be included on WidgetProvider manifest intent action
    // to be recognized by this WidgetProvider to receive broadcast
    public static final String DATA_FETCHED = "mypackage.DATA_FETCHED";
    public static final String EXTRA_LIST_VIEW_ROW_NUMBER = "mypackage.EXTRA_LIST_VIEW_ROW_NUMBER";
    public static final String WIDGET_BUTTON = "mypackage.WIDGET_BUTTON";

    /*
     * this method is called every 30 mins (30 min =30x60x1000) as specified on widgetinfo.xml this
     * method is also called on every phone reboot from this method nothing is
     * updated right now but instead RetmoteFetchService class is called this
     * service will fetch data,and send broadcast to WidgetProvider this
     * broadcast will be received by WidgetProvider onReceive which in turn
     * updates the widget
     */
    @Override
    public void onUpdate(Context ctxt, AppWidgetManager appWidgetManager,
            int[] appWidgetIds) {

        Log.d(TAG, "Hello WidgetProvider onUpdate");

        /*
         * int[] appWidgetIds holds ids of multiple instance of your widget
         * meaning you are placing more than one widgets on your homescreen
         */

        for (int i = 0; i < appWidgetIds.length; ++i) {

            // Create an Intent for on click refresh
            Intent intent = new Intent(WIDGET_BUTTON);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(ctxt, 0,
                    intent, PendingIntent.FLAG_UPDATE_CURRENT);

            // Get the layout for the App Widget and attach an on-click listener
            // to the button
            RemoteViews remoteViews = new RemoteViews(ctxt.getPackageName(),
                    R.layout.widget_layout);
            remoteViews.setOnClickPendingIntent(R.id.refresh_widget,
                    pendingIntent);

            // Tell the AppWidgetManager to perform an update on the current app widget
            appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews);

            //start RemoteFetchService to parse XML in AsyncTask
            Intent serviceIntent = new Intent(ctxt, RemoteFetchService.class);
            serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    appWidgetIds[i]);
            ctxt.startService(serviceIntent);
        }
        super.onUpdate(ctxt, appWidgetManager, appWidgetIds);
    }

    protected PendingIntent getPendingSelfIntent(Context context, String action) {
        Intent intent = new Intent(context, getClass());
        intent.setAction(action);
        return PendingIntent.getBroadcast(context, 0, intent, 0);
    }

    /*
     * It receives the broadcast as per the action set on intent filters on
     * Manifest.xml once data is fetched from RemoteFetchService,it sends
     * broadcast and WidgetProvider notifies to change the data the data change
     * right now happens on ListProvider as it takes RemoteFetchService
     * listItemList as data
     */
    @SuppressWarnings("deprecation")
    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);

        Log.d(TAG, "Hello WidgetProvider onReceive");

        // if RSS Feed was parsed in RemoteFetchService.java
        if (intent.getAction().equals(DATA_FETCHED)) {

            Log.d(TAG, "Data fetched in Widget Provider in OnReceive");

            int appWidgetId = intent.getIntExtra(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
            AppWidgetManager appWidgetManager = AppWidgetManager
                    .getInstance(context);

            // which layout to show on widget
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                    R.layout.widget_layout);

            // RemoteViews Service needed to provide adapter for ListView
            Intent svcIntent = new Intent(context, WidgetService.class);
            // passing app widget id to that RemoteViews Service
            svcIntent
                    .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            // setting a unique Uri to the intent
            svcIntent.setData(Uri.parse(svcIntent
                    .toUri(Intent.URI_INTENT_SCHEME)));
            // setting adapter to listview of the widget
            remoteViews.setRemoteAdapter(appWidgetId, R.id.listViewWidget,
                    svcIntent);
            // setting an empty view in case of no data
            remoteViews.setEmptyView(R.id.listViewWidget, R.id.empty_view);

            // onclick item listview

            // This section makes it possible for items to have individualized
            // behavior.
            // It does this by setting up a pending intent template. Individuals
            // items of a collection
            // cannot set up their own pending intents. Instead, the collection
            // as a whole sets
            // up a pending intent template, and the individual items set a
            // fillInIntent
            // to create unique behavior on an item-by-item basis.
            Intent toastIntent = new Intent(context, WidgetProvider.class);
            // Set the action for the intent.
            // When the user touches a particular view, it will have the effect
            // of
            // broadcasting TOAST_ACTION.
            toastIntent.setAction(WidgetProvider.EXTRA_LIST_VIEW_ROW_NUMBER);
            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);
            remoteViews.setPendingIntentTemplate(R.id.listViewWidget,
                    toastPendingIntent);

            appWidgetManager.updateAppWidget(appWidgetId, remoteViews);

            //appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.listViewWidget);
        }
        // if item on list was clicked
        if (intent.getAction().equals(EXTRA_LIST_VIEW_ROW_NUMBER)) {

            Log.d(TAG, "List Item Clicked in OnReceive in Widget Provider");

            int appWidgetId = intent.getIntExtra(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);

            AppWidgetManager appWidgetManager = AppWidgetManager
                    .getInstance(context);

            // get position on listview which was clicked
            int position = intent.getIntExtra(EXTRA_LIST_VIEW_ROW_NUMBER, 0);

            // get RSSFeed
            RSSFeed feed = ListItem.Feed;

            Intent toastIntent = new Intent(context, ItemDetailActivity.class);
            toastIntent.setAction(WidgetProvider.EXTRA_LIST_VIEW_ROW_NUMBER);
            toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    appWidgetId);

            /*
             * Toast.makeText(context, "Clicked on position :" + viewIndex,
             * Toast.LENGTH_SHORT).show();
             */

            // start ItemDetailActivity
            Intent detailIntent = new Intent(context, ItemDetailActivity.class);
            detailIntent.putExtra("pos", position);
            detailIntent.putExtra("feed", feed);
            detailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(detailIntent);

        }
        // if refresh button was pressed
        if (WIDGET_BUTTON.equals(intent.getAction())) {

            Log.d(TAG,
                    "Refresh Button Clicked in OnReceive in Widget Provider");

            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
            int appWidgetIds[] = appWidgetManager.getAppWidgetIds(
                                       new ComponentName(context, WidgetProvider.class));
            appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.listViewWidget);

        } else {
            Log.d(TAG, "No Data fetched in Widget Provider");
        }

    }
}

RemoteFetchService

public class RemoteFetchService extends Service {

    //Tag for Logging
    private static final String TAG = "Widget";

    private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
    RSSFeed feed;
    public Date pDate;
    public static ArrayList<ListItem> listItemList;

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }

    /*
     * Retrieve appwidget id from intent it is needed to update widget later
     * start Async Task
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Log.d(TAG, "Hello RemoteFetchService onStartCommand");

        if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID))
            appWidgetId = intent.getIntExtra(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);

        // fetchDataFromWeb();
        new AsyncLoadXMLFeed().execute();

        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * AsyncTask which parses the xml and post it
     **/
    public class AsyncLoadXMLFeed extends AsyncTask<Void, Void, RSSFeed> {


        protected RSSFeed doInBackground(Void... params) {
            try {

                Log.d(TAG, "Starte Parsing von URL in Widget");
                // Obtain feed
                DOMParser myParser = new DOMParser();
                feed = myParser.parseXml("http://www.test.de/feed");
                Log.d(TAG,
                        "ItemCount Parser in Widget: " + feed.getItemCount());

                return feed;

            } catch (Exception e) {
                Log.d(TAG, "Exception beim Parsen: " + e.toString());
                return null;
            }
        }

        @Override
        protected void onPostExecute(RSSFeed parsed_feed) {
            // super.onPostExecute(result);
            Log.d(TAG, "Async Parse fertig");
            listItemList = new ArrayList<ListItem>();

            if (feed != null) {
                try {
                    int length = feed.getItemCount();
                    for (int i = 0; i < length; i++) {

                        String date = calc_date_difference(feed, i);

                        final ListItem listItem = new ListItem();
                        ListItem.Feed = feed;
                        listItem.heading = feed.getItem(i).getTitle();
                        listItem.pubDate = date;
                        Log.d(TAG+" Heading", feed.getItem(i).getTitle());
                        Log.d(TAG+" PubDate", date);
                        listItemList.add(listItem);
                    }

                } catch (Exception e) {
                    Log.d(TAG, "Exception onPostExecute: " + e.toString());
                }
            } else {
                Log.d(TAG, "Feed in onPostExecute ist null");
            }

            // start intent and broadcast WidgetProvider, that data is fetched
            Intent widgetUpdateIntent = new Intent();
            widgetUpdateIntent.setAction(WidgetProvider.DATA_FETCHED);
            widgetUpdateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    appWidgetId);
            sendBroadcast(widgetUpdateIntent);

            // stop service
            stopSelf();
        }
    }

    /**
     * Method to calculate the time difference
     **/
    public String calc_date_difference(RSSFeed feed, int pos) {
        // calculate the time difference to the actual system time
        String pubDate = feed.getItem(pos).getDate();
        SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z",
                Locale.ENGLISH);
        try {
            try {
                pDate = df.parse(pubDate);
            } catch (java.text.ParseException e) {
                e.printStackTrace();
            }
            pubDate = "Vor " + DateUtils.getDateDifference(pDate);
            return pubDate;
        } catch (ParseException e) {
            Log.e(TAG, "Error parsing date..");
            return null;
        }
    }
}

ListProvider

/**
 * If you are familiar with Adapter of ListView,this is the same as adapter with
 * few changes
 * 
 */
public class ListProvider implements RemoteViewsFactory {

    // Tag for Logging
    private static final String TAG = "Widget";

    private RemoteViews views;
    private Context ctxt = null;
    private int appWidgetId;
    private ArrayList<ListItem> listItemList = new ArrayList<ListItem>();
    public ImageLoader imageLoader;
    private Bitmap bmp;

    public ListProvider(Context ctxt, Intent intent) {
        this.ctxt = ctxt;
        appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);

        if (RemoteFetchService.listItemList != null)
            listItemList = (ArrayList<ListItem>) RemoteFetchService.listItemList
                    .clone();
        else
            listItemList = new ArrayList<ListItem>();
    }

    @Override
    public void onCreate() {
        // no-op
        Log.d(TAG, "Hello ListProvider onCreate");
    }

    @Override
    public void onDestroy() {
        // no-op
    }

    @Override
    public int getCount() {
        return listItemList.size();
    }

    /*
     * Similar to getView of Adapter where instead of Viewwe return RemoteViews
     */
    @Override
    public RemoteViews getViewAt(int position) {

        final RemoteViews remoteView = new RemoteViews(ctxt.getPackageName(),
                R.layout.widget_row);
        ListItem listItem = listItemList.get(position);
        remoteView.setTextViewText(R.id.heading, listItem.heading);
        remoteView.setTextViewText(R.id.pubDate, listItem.pubDate);

        // onclick item listview
        Intent fillInIntent = new Intent();
        fillInIntent.putExtra(WidgetProvider.EXTRA_LIST_VIEW_ROW_NUMBER,
                position);
        remoteView.setOnClickFillInIntent(R.id.heading, fillInIntent);

        return remoteView;
    }

    @Override
    public RemoteViews getLoadingView() {
        return (null);
    }

    @Override
    public int getViewTypeCount() {
        return (1);
    }

    @Override
    public long getItemId(int position) {
        return (position);
    }

    @Override
    public boolean hasStableIds() {
        return (true);
    }

    @Override
    public void onDataSetChanged() {
        // This code is executed if the refresh button is pressed or after the
        // update period

        // start RemoteFetchService to parse XML in AsyncTask
        Intent serviceIntent = new Intent(ctxt, RemoteFetchService.class);
        serviceIntent
                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
        ctxt.startService(serviceIntent);
    }
}

widget_provider.xml

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  android:minHeight="200dp"
  android:minWidth="200dp"
  android:updatePeriodMillis="30000"
  android:initialLayout="@layout/widget_layout"
  android:autoAdvanceViewId="@+id/words"
  android:previewImage="@drawable/widget_preview"
  android:resizeMode="vertical|horizontal"
/>

I hope this all you need to assist me.

Upvotes: 3

Views: 4171

Answers (3)

Emanuel Moecklin
Emanuel Moecklin

Reputation: 28856

Before I give you a potential answer to your questions, here're some ideas how to improve the code (note they are related to the answer as they help prevent the error from happening).

onReceive

When you implement onReceive() you have to keep in mind that appWidgetIds can be passed either as an int with AppWidgetManager.EXTRA_APPWIDGET_ID or as int array with AppWidgetManager.EXTRA_APPWIDGET_IDS (you use both methods yourself). To deal with this I usually do:

@Override
public void onReceive(Context context, Intent intent) {
    Bundle extras = intent.getExtras();
    int invalidId = AppWidgetManager.INVALID_APPWIDGET_ID;
    int appWidgetId = extras != null ? extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, invalidId) : invalidId ;
    int[] appWidgetIds = extras != null ? extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS) : null;

    // single appWidgetId as parameter
    if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
        appWidgetIds = new int[] {appWidgetId};
    }

    // multiple appWidgetIds as parameter
    if (appWidgetIds != null) {
        for (int id:appWidgetIds) {
            // process a single appWidgetId
            onReceiveInternal(context, intent, id)
        }
    }

    super.onReceive(context, intent);
}

private boolean onReceiveInternal(Context context, Intent intent, int appWidgetId) {
    // do your stuff for one appWidgetId
}

That would prevent you from doing this:

if (WIDGET_BUTTON.equals(intent.getAction())) {
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    int appWidgetIds[] = appWidgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.listViewWidget);
}

If you receive a broadcast for a single appWidgetId you should not update all widget instances. Why not? Because all your widget instances will receive the same broadcast and each one will call the notifyAppWidgetViewDataChanged for the other instances. If the user puts 4 instances of your widget on the home screen, pressing the refresh button would trigger 16 (sic!) updates.

onUpdate

When you process the DATA_FETCHED broadcast you want to update the widget. Now why would you use different code for this compared to onUpdate()? They should really be doing the same so I'd suggest to use the following pattern:

private void onReceiveInternal(Context context, Intent intent, int appWidgetId) {
    AppWidgetManager appWidgetMgr = AppWidgetManager.getInstance(context);
    String action = intent.getAction();

    if (DATA_FETCHED.equals(action)) {
        onUpdateInternal(context, appWidgetMgr, appWidgetId);
    }
    // more code here
}

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    for (int appWidgetId : appWidgetIds) {
        onUpdateInternal(context, appWidgetManager, appWidgetId);
        startFetchService(context, appWidgetId); // here we start the service to fetch the feed
    }
    super.onUpdate(context, appWidgetManager, appWidgetIds);
}


private void onUpdateInternal(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
    // here goes the update code
}

Doing that guarantees that updating the widget does the same regardless whether it's an Android triggered update or one triggered when your feed data comes in. I understand that you want to start the fetch service in the first case (which can easily be achieved as my code sample above shows), nevertheless the actual ui update should be doing the same.

Right now you're not creating the same RemoteViews. If it's an Android triggered update then you basically inflate the layout and add an Intent for the refresh button while the update after the feed comes in uses a RemoteViewsService to have individual Intents for each list item. The problem with that approach is that an Android update would destroy the individual Intents till the new feeds come in and the behavior of the widget would be erratic from a user point of view. With my approach the display and behavior would be consistent.

manifest

Please make sure you include the tag with the correct actions in the definition for your WidgetProvider receiver or you won't receive any updates (I guess you already have that or your widget wouldn't show any data at all):

<receiver android:name="mypackage.WidgetProvider">
    <intent-filter>
        <action android:name="mypackage.DATA_FETCHED" />
        <action android:name="mypackage.EXTRA_LIST_VIEW_ROW_NUMBER" />
        <action android:name="mypackage.WIDGET_BUTTON" />
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/widget_provider" />
</receiver>

One answer

You didn't post the complete code so I'm not able to run it. Without that I can only pinpoint what part will definitely fail but I might miss other parts that might not work either. Thus my declaration of this as "one answer".

When creating your RemoteViews after receiving the DATA_FETCHED broadcast you don't set an Intent to the refresh button (which you do in your onUpdate but that one isn't called when processing the DATA_FETCHED -> hence my suggestion to use the same update code which would prevent errors like this from happening). Without that Intent pressing the refresh button won't do anything.

Of course the other answers are correct when it comes to the periodical update which happens only every 30 minutes according to this: http://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo.html#updatePeriodMillis

If you need more frequent updates use AlarmManager (search on SO for code samples).

Upvotes: 7

Hassaan Rabbani
Hassaan Rabbani

Reputation: 2465

The issue is:

You have specified the period for update just 30 seconds in the xml

android:updatePeriodMillis="30000"

You need to wait for complete 30 minutes for making it work. see link for details

http://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo.html#updatePeriodMillis

Upvotes: 0

Bryan Herbst
Bryan Herbst

Reputation: 67209

Though this won't solve your problem with updating via your button, your updatePeriodMillis will definitely not work as expected.

As per the AppWidgetProviderInfo documentation, the minimum update period is 30 minutes (1800000ms). You specify an update period of just 30 seconds in your XML. If you haven't been waiting a full 30 minutes before checking the widget, you won't see any new data.

Upvotes: 3

Related Questions