0

The Problem

My AppWidget isn't updating when it should. I can't figure out why.

Relevant Code

Here is my app_widget_info.xml file. updatePeriodMillis is set to zero because I update the widget manually using the AlarmManager:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="250dp" android:minHeight="110dp" android:updatePeriodMillis="0"
    android:initialLayout="@layout/app_widget"
    android:widgetCategory="home_screen|keyguard"
    android:initialKeyguardLayout="@layout/app_widget"
    android:previewImage="@drawable/preview_image" />

Here are the relevant portions of my AppWidgetProvider class:

public class MyAppWidget extends AppWidgetProvider {

    private static PendingIntent service1 = null, service2 = null, update_service = null;

    public static void nullifyService( String service_name ) {
        if ( service_name.equals( "service1" ) ) {
            service1 = null;
        } else {
            service2 = null;
        }
    }

    public static void updateMyWidget(final Context context, AppWidgetManager appWidgetManager, int appWidgetId) {

        /* ... code to update my widget ... */
        /* ... this code is tested and I know it works ... */

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {

        // set alarms if necessary for the beginning and end of the next void
        if ( null ==  update_service || ( null == service1 && null == service2 ) ) {

            final AlarmManager m = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);

            // set alarms for the beginning and end of the upcoming/current event window
            if ( null == service1 && null == service1 ) {
                final Calendar s1_c = Calendar.getInstance(), // service1 alarm time
                               s2_c = Calendar.getInstance(); // service2 alarm time
                final Intent   s1_i = new Intent(context,MyService.class).putExtra( "serviceName", "service1" ),
                               s2_i = new Intent(context,MyService.class).putExtra( "serviceName", "service2" );
                final long s1_millis, s2_millis;

                // get the current VOC info
                if ( db == null ) db = new MyDataBase(context);
                alarm_info = db.getCurrentAlarms();

                // alarm times for service1 and service2 are UNIX timestamps
                // pulled from a sqlite database (tested and working fine)
                s1_c.setTime(new Date(alarm_info.getLong(4) * 1000 ));
                s2_c.setTime(new Date(alarm_info.getLong(1) * 1000 ));

                // if it is still before the next service1 alarm time
                // set an alarm for the exact time
                if ( s1_c.getTimeInMillis() > Calendar.getInstance().getTimeInMillis() ) {
                    s1_c.set(Calendar.MILLISECOND, 0);
                    s1_c.set(Calendar.SECOND, 0);
                    service1 = PendingIntent.getService(context, 0, s1_i, PendingIntent.FLAG_ONE_SHOT );
                    s1_millis = SystemClock.elapsedRealtime() + s1_c.getTime().getTime() - System.currentTimeMillis();
                    m.setExact(AlarmManager.ELAPSED_REALTIME, s1_millis, service1);
                }

                // set the alarm for the service2 alarm time
                s2_c.set(Calendar.MILLISECOND, 0);
                s2_c.set(Calendar.SECOND, 0);
                service2  = PendingIntent.getService(context, 1, s2_i, PendingIntent.FLAG_ONE_SHOT );
                s2_millis = SystemClock.elapsedRealtime() + s2_c.getTime().getTime() - System.currentTimeMillis();
                m.setExact(AlarmManager.ELAPSED_REALTIME, s2_millis, service2);
            }

            // set the widget to update every 15 minutes regardless
            if ( null == update_service ) {
                final Intent up_i = new Intent(context,MyService.class).putExtra( "serviceName", "update" );
                update_service = PendingIntent.getService(context, 2, up_i, PendingIntent.FLAG_CANCEL_CURRENT);
                m.setRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime(), 15 * 60 * 1000, update_service);
            }
        }

        // There may be multiple widgets active, so update all of them
        for( int appWidgetId : appWidgetIds) {
            updateMyWidget(context, appWidgetManager, appWidgetId);
        }
    }

    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
        final AlarmManager m = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
        m.cancel(service1);
        m.cancel(service2);
        m.cancel(update_service);
    }
}

And here are the relevant portions of my Service class:

public class MyService extends Service {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId){

        // update all of the widgets
        ComponentName cn = new ComponentName(this, MyAppWidget.class);
        AppWidgetManager m = AppWidgetManager.getInstance(this);
        for ( int awid : m.getAppWidgetIds(cn) ) {
            MyAppWidget.updateMyWidget(this,m,awid);
        }
        String service_name = intent.getStringExtra( "serviceName" );
        if ( "service1" == service_name || "service2" == service_name ) {
            MyAppWidget.nullifyService(service_name);
        }
        return START_NOT_STICKY;
    }
}

What I Know and What I've Tried

  1. The update_service works fine and updates the widget dependably every 15 minutes
  2. The alarms for service1 and service2 are being set.

Here is a snippet from running adb shell dumpsys alarm showing one of the repeating alarms (that fires every 15 minutes, or 900000ms):

ELAPSED #0: Alarm{41a9e1b8 type 3 com.mycompany.myappwidget}
    type=3 whenElapsed=231265839 when=+11m52s295ms window=-1 repeatInterval=900000 count=0
    operation=PendingIntent{41dbf158: PendingIntentRecord{41cf7920 com.mycompany.myappwidget startService}}

And here is another showing one of the alarms that is set to fire at an exact time:

ELAPSED #4: Alarm{433d1560 type 3 com.mycompany.myappwidget}
    type=3 whenElapsed=90066209 when=+4h34m36s121ms window=0 repeatInterval=0 count=0
    operation=PendingIntent{41f0ec28: PendingIntentRecord{41f1e690 com.mycompany.myappwidget startService}}

And here are the stats for the alarms that have already fired where d and c refer to the services that got started:

com.mycompany.myappwidget +1s857ms running, 0 wakeups:
    +1s817ms 0 wakes 83 alarms: cmp={com.mycompany.myappwidget/com.mycompany.myappwidget.d}
    +40ms 0 wakes 1 alarms: cmp={com.mycompany.myappwidget/com.mycompany.myappwidget.c}

As you can see, the alarms are being triggered, but the interface is not updating.

  1. Since the repeating alarm fires reliably every 15 minutes, I don't think that the OS is killing them based on the restriction of only updating widgets every 30 minutes
  2. I have tried creating a different Service class for each alarm in order to avoid the alarms canceling each other as is suggested by this post
  3. I have used Log statements to confirm that the updateMyWidget() function is actually being called when each alarm is triggered. It appears that the function is called, but doesn't always update the widgets.
  4. I have tried setting different unique requestCodes for each PendingIntent so that when a new alarm is set, it doesn't inadvertently cancel the PendingIntents set by other alarms

Questions and Ideas

Am I doing this wrong? On average, updates only seem to take about 200ms, so perhaps I should be using PendingIntent.getBroadcast() and calling android.appwidget.action.APPWIDGET_UPDATE instead of creating a Service to handle the updates. This is driving me nuts, so any hints or clues that might point me in the right direction would be GREATLY appreciated.

UPDATE

@Karakuri's answer taught me how to differentiate PendingIntent objects for the purposes of not having them overwrite each other when setting alarms. Here's how I fixed this code:

  1. I changed the putExtra() calls to setAction() and set the action as one of the predefined constants, e.g. Intent.ACTION_MAIN, that seemed semantically similar to what the service was doing
  2. I removed all of the services from the class-level scope, as it turns out it was unnecessary to keep them around in between calls to onUpdate()
  3. I got rid of all of the conditional logic inside of onUpdate()
  4. I went back to using the built-in android update mechanism, i.e. changed the android:updatePeriodMillis in app_widget_info.xml to 1800000 (30 minutes) and got rid of the repeating alarm I'd set with setRepeating()
  5. I discovered that there was a problem with the boundary conditions for my alarms that was preventing the UI from updating when I thought it should--I wouldn't have discovered this, though, if I hadn't started using setAction()

Thank you very much!!!

Community
  • 1
  • 1
morphatic
  • 6,397
  • 4
  • 42
  • 56
  • I have a few suggestions, but I don't know which (if any) will solve your problem as I'm not entirely sure where the problem is. – Karakuri Jul 24 '15 at 02:51

1 Answers1

3

Here are my suggestions, hopefully somewhere in here is something that fixes your issue.

  1. Don't use == to compare strings in your service (or anywhere). Use .equals() with named constants.

  2. Your PendingIntents only differ by extras. When you give the same Intent to PendingIntent, it only keeps one of them. For the purposes of comparing Intents, extras are not considered (see the documentation of PendingIntent about this). Instead of using the extras, I would give each Intent a different action and use intent.getAction() in your Service.

  3. Don't rely on static variables for keeping state. Your process can be shut down any time and you will lose that data. Your AppWidget is running in a different process, so unless you have something else running, Android may decide to clear your process. I would use some kind of persistent storage, probably SharedPreferences if it's just for a handful of keys.

Karakuri
  • 36,506
  • 12
  • 75
  • 103
  • tyvm!! per suggestion 3, since my static class variables are objects and not scalars, would you recommend [this method](http://stackoverflow.com/a/15588512/296725) for storing the objects for later retrieval? Also is that Karakuri as in からくりテレビ? – morphatic Jul 24 '15 at 03:21
  • I wouldn't store them. I see you are keeping them around so you can cancel them in `onDisabled()`, but really I think it's better to just reconstruct the `PendingIntent`s. You can refactor that logic into a method to make sure they are built the same way each time. – Karakuri Jul 24 '15 at 07:33