I'm creating a fairly common use case of AppWidget on Android.
AppWidgetProvider calls onAppWidgetOptionsChanged() (stretchable widget) and onUpdate() (timed)
From those methods I start an IntentService. Case coming from options changed I pass the new size in the Intent.
The service contacts a web-service, builds the RemoteViews and calls updateAppWidget()
my main test device is a Nexus 7 (2012) running stock 4.3 (stock Launcher)
The widget does not use RemoteViewFactory and does not user AlarmManager. It's a static view with a constant time defined in XML.
It works most of the times, but sometimes the call to updateAppWidget() is completely ignored by the Launcher and no update happens on the screen. If I force close the launcher, clear it caches and re-size the widget (forcing an update) then it updates.
I believe there's something to do with frequency of update because I tricked up some stuff in the IntentService to, whenever it's resizing, only call to the last intent (when the user stops messing with the widget) and it soften a bit the issue.
Let's show some simplified code (it's very standard, i believe):
public class AlbumWidgetService extends WidgetUpdateIntentService {
#Override
protected void onHandleIntent(Intent intent) {
// get's widgetID or array of IDs and pass to 'doTheJob'
}
private void doTheJob(int appWidgetId, int heightInDp, int widthInDp) {
// ...
// here goes code with pre calculations and get data
// ...
// create Intent and PendingIntent with some extras
Intent intent = ... etc
PendingIntent pi = PendingIntent.getActivity( ... etc
// get url for some images
List<String> imageFilenames = getImagesFilename(albumId, totalImages);
// Create the remote view
RemoteViews views = new RemoteViews(getPackageName(), R.layout.album_widget);
// ...
// here goes a bunch of code that load bitmaps from the URLs
// set text and colors in the remote view
// put ImageViews into the remote view, etc
// ...
try {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
appWidgetManager.updateAppWidget(appWidgetId, views);
Log.d(this, "Updating the widget id " + appWidgetId);
} catch (Exception e) {
// this exception happens if the RemoteView is too big, have too many bitmaps.
// I'm already minizing this to happen with the pre calculations, but better safe than sorry
Log.e(this, "Failed to update the widget id " + appWidgetId, e);
}
}
as I said, the thing mostly works (I can see the Log and I can see the on-screen result. But every once in a while it does not update after a resize, even thou I can see the Log and it did not crashes or anything.
ideas?
Related
I'm adding a widget to an old app which I'm updating from a service I'm using to poll for data in the background (on an alarm). I update the widget every time the service gets a result. This is currently working correctly.
// Called from inside my service when it has results
private void updateWidget(List<Earthquake> earthquakes) {
AppWidgetManager manager = AppWidgetManager.getInstance(this);
int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(this, WhatsShakingWidgetProvider.class));
if (appWidgetIds == null || appWidgetIds.length == 0)
return;
Earthquake earthquake = earthquakes.get(0);
RemoteViews views = new RemoteViews(getPackageName(), R.layout.widget_detail);
// Update views
views.setTextViewText(R.id.widget_detail_latest_magnitude, earthquake.getFormattedMagnitude());
// etc...
// Update each widget
for(int appWidgetId : appWidgetIds) {
manager.updateAppWidget(appWidgetId, views);
}
}
This polling service is optional; it can be turned on or off in the app's settings.
If the service is off when the user adds the widget, the widget_error layout is shown, as expected. The user can tap on the widget to enter the settings and turn the background updates on. When they do this (turn the setting on or off), I broadcast ACTION_APPWIDGET_UPDATE. The widget enters onUpdate correctly, and is updated correctly by the service the next time it runs (I've set it up so the widget triggers a service call in onUpdate - see below).
The widget does not correctly display the widget_error layout when the service becomes disabled after being enabled - it leaves the old layout in place, even though all the disabled-case code is run.
This is the code that gets called when the user toggles the setting (Source):
// If our user has widgets, we should update those - let the widget do the updating depending on the prefs, though.
Intent intent = new Intent(this, WhatsShakingWidgetProvider.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
// Use an array and EXTRA_APPWIDGET_IDS instead of AppWidgetManager.EXTRA_APPWIDGET_ID,
// since it seems the onUpdate() is only fired on that:
int[] ids = { R.xml.widget_info };
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
sendBroadcast(intent);
And this is the code in onUpdate which should be updating the widgets, but isn't:
#Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean backgroundUpdatesEnabled = prefs.getBoolean(PreferenceActivity.KEY_PREF_ALLOW_BG_NOTIFICATIONS,
DefaultPrefs.BG_NOTIFICATIONS_ENABLED);
if (!backgroundUpdatesEnabled) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_error);
// Update click to take to preferences
Intent intent = new Intent(context, PreferenceActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.widget_error_parent_container, pendingIntent);
// Update each widget
appWidgetManager.updateAppWidget(appWidgetIds, views);
} else {
// Let's get some data for the user! Service does the work of updating the views.
WakefulIntentService.sendWakefulWork(context, GeonetService.class);
}
}
There are no errors logged in Logcat. Stepping through this, I correctly enter each part of the if when expected (that is, if the user turned the setting off, then I create RemoteViews views as widget_error, otherwise I start the service).
Why does the widget_error layout display correctly the first time through onUpdate, but not when the user enables, then disables, the background update setting?
I've tried wrapping this in a RelativeLayout and setting the visibility of the error message/the content, but that exhibited the same behaviour - I couldn't get the error message to show back up after initially hiding it.
I ended up duplicating the code in two places (the preferences activity and the widget provider) and it worked. The only variable appears to be the Context object.
It appears that for some reason the Context instance you get in the AppWidgetProvider (that is, in onUpdate) only works the first time - or, doesn't work when I send the broadcast myself. I'm not sure why.
I pulled my duplicated code out to a separate class and just pass in the Context instance I have available, whether it's the Service, an Activity, or the AppWidgetProvider (which is a BroadcastReceiver). This correctly updates the widget, and I can call it from anywhere I have a Context.
Source is available here.
I'm a noob to android and I'm having issues updating an appwidget. It's a news widget that displays different text every 20 secs. I have no problem getting the text to switch & display properly when the widget is initialized. However, after every 30 min update of the widget my widgetID int array retains the int that existed prior to the update. So after each update the widget shows old data and new data. Is there a to clear the widget ID int array of old data during the update process. Any help is greatly appreciated.
My Code:
allWidgetIds2 = appWidgetManager.getAppWidgetIds(thisWidget);
Method for switching text in widget. This works fine initially, but after update shows old data along with new data...
public void updateStory() {
tickerheadline = RssReader.rssheadline.get(storyCounter);
tickerstory = RssReader.rssstory.get(storyCounter);
remoteViews.setTextViewText(R.id.headline, tickerheadline );
remoteViews.setTextViewText(R.id.story, tickerstory );
if (storyCounter==RssReader.rssheadline.size()-1){
storyCounter = 0;
storyCounter++;
}else{
storyCounter++;
}
appWidgetManager.updateAppWidget(allWidgetIds, remoteViews);
//Log.e("Widget", allWidgetIds.toString());
mHandler.postDelayed(new Runnable() {
public void run() {
updateStory();
} } ,20000); }
}
EDIT
public void updateStory() {
//Added
appWidgetManager = AppWidgetManager.getInstance(this.getApplicationContext());
ComponentName thisWidget = new ComponentName(getApplicationContext(),MyWidgetProvider.class);
remoteViews = new RemoteViews(this.getApplicationContext().getPackageName(),R.layout.widget1);
tickerheadline = RssReader.rssheadline.get(storyCounter);
tickerstory = RssReader.rssstory.get(storyCounter);
remoteViews.setTextViewText(R.id.headline, tickerheadline );
remoteViews.setTextViewText(R.id.story, tickerstory );
if (storyCounter==RssReader.rssheadline.size()-1){
storyCounter = 0;
storyCounter++;
}else{
storyCounter++;
}
appWidgetManager.updateAppWidget(thisWidget, remoteViews);
//appWidgetManager.updateAppWidget(allWidgetIds, remoteViews);
//Log.e("Widget", allWidgetIds.toString());
mHandler.postDelayed(new Runnable() {
public void run() {
updateStory();
} } ,20000); }
}
Although you can be very clever with AppWidgets in android in terms of updating and so in, the easiest way to get going is simply to rebuild the widget contents every time you refresh. As you're only refreshing every 30 minutes, there's not much reason to worry about cpu usage.
So, I would suggest doing a standard rebuild each time you trigger an update using the standard approach. The code you have above, whilst it looks correct, doesn't actually force an update, much cleaner to start from scratch. Once that's working, then if you can make it more lightweight, do tell me how!
// You need to provide:
// myContext - the Context it is being run in.
// R.layout.widget - the xml layout of the widget
// All the content to put in it (duh!)
// Widget.class - the class for the widget
RemoteViews rv= new RemoteViews(myContext.getPackageName(), R.layout.widget);
rv.setXXXXXXX // as per normal using most recent data set.
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(myContext);
// If you know the App Widget Id use this:
appWidgetManager.updateAppWidget(appWidgetId, rv);
// Or to update all your widgets with the same contents:
ComponentName projectWidget = new ComponentName(myContext, Widget.class);
appWidgetManager.updateAppWidget(projectWidget, rv);
Often the easiest approach (in my view) is to wrap this up in a WidgetUpdater.class which you either call from a service or directly using a time alarm call. Using onUpdate is generally a bad idea because it force the phone into full power mode and isn't very controllable. I've never managed to make it work. Much better to do something like:
Intent myIntent = // As per your onUpdate Code
alarmPendingIntent = PendingIntent.getService(context, 0, myIntent, PendingIntent.FLAG_ONE_SHOT);
// ... using the alarm manager mechanism.
AlarmManager alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC_WAKEUP, nextUpdateTimeMillis, alarmPendingIntent);
I will make it simple. I have my widget code. My widget layout contains a linear layout with one button in it. In my widget code, I initialize a String List with some values in it.
When I click the button in my widget, I have to update my List with some more values.
So, this is my code,
List<String> myList = null;
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
myList = new List<String>();
myList.add("1");
myList.add("2");
...
Intent intent = new Intent(context, getClass());
intent.setAction("CALL_UPDATE");
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.button, pendingIntent);
....
}
#Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("CALL_UPDATE")) {
Toast.makeText(context, "Intent received"+myList , 2000).show();
}
}
When the button is clicked, the broadcast is properly received by onReceive(). The problem is, on onReceive(), I see my list as null instead of some values in it as some string objects were added earlier.
Can anyone help?
Thx!
Rahul.
Documentation says:
This has important repercussions to what you can do in an
onReceive(Context, Intent) implementation: anything that requires
asynchronous operation is not available, because you will need to
return from the function to handle the asynchronous operation, but at
that point the BroadcastReceiver is no longer active and thus the
system is free to kill its process before the asynchronous operation
completes.
In particular, you may not show a dialog or bind to a service from
within a BroadcastReceiver. For the former, you should instead use the
NotificationManager API. For the latter, you can use
Context.startService() to send a command to the service.
And:
onReceive() is normally called within the main thread of its process,
so you should never perform long-running operations in it (there is a
timeout of 10 seconds that the system allows before considering the
receiver to be blocked and a candidate to be killed). You cannot
launch a popup dialog in your implementation of onReceive().
Edit:
AppWidgetProvider is a BroadcastReceiver and it's instance (and so it's fields) will be deleted after it's lifcycle.When you create a new instance of widget in HomeScreen,onUpdate and onReceive of AppWidgetProvider invoke and list of this instance of AppWidgetProvider is not null.But after invoking onReceive (for example 10 seconds)this instance will delete.When you click on button ,second instance of AppWidgetProvider will be create and it's list is null.
You can save your list public static field of a class and retrieve it when you need.
My widget gets data from the internet every 3 minutes, some are displayed directly on the widget and others are stored in SharedPreferences so when the user taps on the widget that information appears as a dialog. When having more than one widget running, no matter which widget I click the log says the appWidgetId comes from one of them always
My problem seems to be the way I'm declaring the widget's setOnClickPendingIntent(). I'm doing this inside the service, right before fetching the data and since the same service is run by every (widget) AlarmManager, every widget gets the PendingIntent from the last service ran.
public class WidgetService extends Service
{
#Override
public void onStart(Intent intent, int startId)
{
Intent intentUmbrales = new Intent(context, LaunchUmbralesDialog.class);
intentUmbrales.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent pendingIntentUmbrales = PendingIntent.getActivity(context,0,intentUmbrales,0);
// Get the layout for the App Widget and attach an on-click listener to the button
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
// views.setOnClickPendingIntent(R.id.energia_widget, pendingIntentImei);
views.setOnClickPendingIntent(R.id.imageLogo_widget, pendingIntentUmbrales);
//..then I fetch data, store the rest in SharedPreferences and update widget remoteViews
}
}
How can I avoid this? How can I make an individual "button" for each widget with getting them overlapped? Also note that I've already tried to declare those PendingIntents in the AppWidgetProvider's onUpdate() method (inside a loop for every appWidgetId from the array given by the method)
Thanks in advance!
Regards, Rodrigo.
When declaring the .setOnClickPendingIntent() first add to the Intent
Uri data = Uri.withAppendedPath(
Uri.parse(URI_SCHEME + "://widget/id/")
,String.valueOf(appWidgetId));
intent.setData(data);
so that each widget gets a unique ID and they don't get messed up!
I have a really simple appwidget (two text views and a button). I've tested it on a Touchpad, Droid 1, and a Droid Razr. It works on everything except the Razr. When I add the widget to the homescreen it doesn't display; it's just invisible. If I hold down on the spot where it would be it selects a widget and if I move it around I see other widgets move out of the way but it's completely invisible.
I put some Toasts in the onReceive and onEnabled methods and the Toast displays all the right information (ie intent action and extras).
Anybody have any experience with this?
EDIT: Please keep in mind this is just for debugging and does not follow best practices
public class GoogleTalkWidget extends AppWidgetProvider {
Button sendMessage;
#Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Received Intent Action = " +
intent.getAction(), Toast.LENGTH_SHORT).show();
if(intent.getAction().equals(Utils.RECEIVED_MESSAGE_WIDGET)){
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.main_widget);
views.setTextViewText(R.id.widget_message,
(CharSequence)intent.getStringExtra("MESSAGE"));
views.setTextViewText(R.id.widget_sender,
(CharSequence)intent.getStringExtra("SENDER"));
Toast.makeText(context, "Received " +
intent.getStringExtra("MESSAGE") + " FROM " +
intent.getStringExtra("SENDER"), Toast.LENGTH_SHORT).show();
ComponentName cn = new ComponentName(context,
GoogleTalkWidget.class);
AppWidgetManager.getInstance(context).updateAppWidget(cn, views);
}
super.onReceive(context, intent);
}
#Override
public void onEnabled(final Context context){
super.onEnabled(context);
Toast.makeText(context, "Enabled", Toast.LENGTH_SHORT).show();
}
}
The best thing I can think of (supposed there isn't a bug or other problem with the launcher of the Razr) is that your resources aren't configured correctly. Maybe the Razr has a different dpi density and there aren't resources for that density in your project.
Try for example to move all of the drawables that make up your widget to the folder res\drawable-nodpi and see how it's going.
EDIT: I saw something strange in your code
In your GoogleTalkWidget class onReceive method, your widget is only updated when the message Utils.RECEIVED_MESSAGE_WIDGET is received. I don't know what this message is for but an app-widget, the first time is added on the home screen, it receives the android.appwidget.action.APPWIDGET_UPDATE and any other intent filters are registered in the manifest file and they are broadcasted by the system at that time (and the sticky broadcasts intents of course).
If I was in your position I would change the onReceive method as follows:
#Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Received Intent Action = " +
intent.getAction(), Toast.LENGTH_SHORT).show();
super.onReceive(context, intent);
String msg = "No messages yet";
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.main_widget);
if(intent.getAction().equals(Utils.RECEIVED_MESSAGE_WIDGET)){
msg = intent.getStringExtra("MESSAGE");
views.setTextViewText(R.id.widget_sender,
(CharSequence)intent.getStringExtra("SENDER"));
Toast.makeText(context, "Received " +
intent.getStringExtra("MESSAGE") + " FROM " +
intent.getStringExtra("SENDER"), Toast.LENGTH_SHORT).show();
ComponentName cn = new ComponentName(context,
GoogleTalkWidget.class);
}
views.setTextViewText(R.id.widget_message, msg);
AppWidgetManager.getInstance(context).updateAppWidget(cn, views);
}
and see what happens.
If your widget fails to appear then it could be a problem with Razr but this is unlike because I suppose that a whole bunch of other widgets works fine.
Furthermore, although your code is for debugging only, your approach is a little bit problematic. The best place to update your widget views is in the onUpdate method of the AppWidgetProvider and not in the onReceive. Android provides the onUpdate method for that purpose and the onReceive to inform you that a registered broadcast has arrived. The basic difference is that in onUpdate method, Android has extracted all the needed parameters for you from the received Intent extras. One more thing about widget updates is that you should provide an android:updatePeriodMillis value other than 0 (2100000 is a good value) in your widget xml file even if you don't want periodic updates for your widget. I saw somewhere that the onReceive method may not be called if this value is 0.
Keep in mind also that AppWidgetProvider as a Broadcast Receiver lives only as long as the onReceive method does its job, after that is is destroyed thus it is not a good place for "interactive" code like UI listeners etc. I am telling you this because you have a Button declaration (Button sendMessage;) in the top of your GoogleTalkWidget class.
Hope this helps...