Reputation: 2028
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
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
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
Upvotes: 0
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