4

I have a general question about setting up a daily data check from my app (giving a notification when some event occurs).

My current approach is:

  • From the app (onCreate) I start a service
  • From the service I set an alarm using AlarmManager after checking if there already is a matching PendingIntent (in which case I don't need to set a new alarm)
  • The service checks the data from SharedPreferences and eventually shows a notification

For testing purposes I set the alarm to +1 minute ... but it didn't work that way. Maybe I'm doing something wrong conceptionally?

Some code

AndroidManifest.xml

<application
    <activity
        android:name=".OverviewActivity"
        android:label="@string/app_name"
        android:theme="@style/AppTheme.NoActionBar"
        android:configChanges="orientation"
        android:screenOrientation="portrait">

        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <service
        android:name="xxxxx.CheckDaysMonths" >
    </service>
</application>

Service CheckDaysMonths

public class CheckDaysMonths extends Service {

    private static int ALARM_ID = 131313;
    private static String DECLARED_ACTION = "xxxxx.CheckDaysMonths";

    @Override
    public void onCreate() {
        super.onCreate();

        PendingIntent alarmIntent = AlarmHelper.getPendingIntentFromAlarm(this, ALARM_ID, DECLARED_ACTION);
        if(alarmIntent == null) {
           if(someConditionIsMet)
              sendNotification(" my notification text ");
           // Set an alarm for the next time this service should run:
           setAlarm();
        }

        stopSelf();
    }

    public void setAlarm() {

        AlarmHelper.setAlarm(this, ALARM_ID, DECLARED_ACTION, 8);
    }

    public void sendNotification(String notifyText) {

        Intent mainIntent = new Intent(this, OverviewActivity.class);
        @SuppressWarnings("deprecation")
        Notification noti = new Notification.Builder(this)
                .setAutoCancel(true)
                .setContentIntent(PendingIntent.getActivity(this, 131314, mainIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT))
                .setContentTitle("Gratuliere!")
                .setContentText("Du bist seit " + notifyText + " rauchfrei!")
                .setDefaults(Notification.DEFAULT_ALL)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setTicker("Du bist seit " + notifyText + " rauchfrei!")
                .setWhen(System.currentTimeMillis())
                .getNotification();

        NotificationManager notificationManager
                = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(131315, noti);

    }

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

Inside the main activity (OverviewActivity.java)

EDIT: Changed service call from implicit to explicit

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_overview);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    startService(new Intent(this, CheckDaysMonths.class));


    // additional stuff happening on the activity of my app goes here:
    ...
}

AlarmHelper.java

public class AlarmHelper {

    public static PendingIntent getPendingIntentFromAlarm(Context context, int alarmId, String declaredAction) {
        return (PendingIntent.getService(context, alarmId,
                new Intent(declaredAction),
                PendingIntent.FLAG_NO_CREATE));
    }

    public static void setAlarm(Context context, int alarmId, String declaredAction, int hourOfDay) {
        Intent serviceIntent = new Intent(declaredAction);
        PendingIntent pi = PendingIntent.getService(context, alarmId, serviceIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        // How long until tomorrow to its hourOfDay?
        //DateTime tomorrow = (new DateTime()).plusDays(1);
        //DateTime tomorrowAtHourOfDay = new DateTime(tomorrow.getYear(), tomorrow.getMonthOfYear(), tomorrow.getDayOfMonth(), hourOfDay, 0);

        DateTime tomorrowAtHourOfDay = (new DateTime()).plusHours(3);

        AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        am.set(AlarmManager.RTC_WAKEUP, tomorrowAtHourOfDay.getMillis(), pi);
    }
}
devnull69
  • 16,402
  • 8
  • 50
  • 61
  • What do you mean, exactly, by "it didn't work that way"? – Mike M. Feb 04 '16 at 10:59
  • The alarm didn't seem to fire ... I set a break point in debug mode and it didn't go into "onCreate" of the service after the expected alarm timeout expired – devnull69 Feb 04 '16 at 11:00
  • Well, I'm not sure what to suggest. You've omitted some relevant stuff, your `AlarmHelper.setAlarm()` call in the Service doesn't match the signature in the class, your description doesn't quite fit the code, etc. Does your Service start when it's called from the Activity's `onCreate()`? I wouldn't think so, because you're using an implicit Intent, but the `` element in the manifest has no filter for it. Also, that's not allowed as of Lollipop, I think it was. Services need to be started with explicit Intents; i.e., Intents that specify the Service class. – Mike M. Feb 04 '16 at 11:10
  • Hm ok I get it . So I changed the service intent (in OverviewActivity.java) from implicit to explicit `new Intent(this, CheckDaysMonths.class)` which worked well, but I cannot seem to change the serviceIntent (in CheckDaysMonths.java) to explicit, because there only seems to be a way to detect a set alarm if it had been set implicitly before. – devnull69 Feb 04 '16 at 12:09
  • "there only seems to be a way to detect a set alarm if it had been set implicitly before" - I don't think that's right. As long as your `alarmId` and `serviceIntent` are the exact same as when you created the PendingIntent, it should work for explicit Intents as well. In fact, I just tested it, and it works as expected for me. – Mike M. Feb 04 '16 at 13:18
  • This is much better. I'm using explicit intents everywhere now. But the call to `PendingIntent.getService` will be `!=null` when the alarm has been set and not yet fired (which is fine) but also when the alarm has just been fired. In that case my code will not run the scheduled data check. So one question remains: How can I see the difference between a fired and an "unfired" alarm? – devnull69 Feb 04 '16 at 14:05
  • If I'm following you correctly, you can call `cancel()` on the `PendingIntent` when the alarm fires. Then the next call to `getService()` with `FLAG_NO_CREATE` will return null. – Mike M. Feb 04 '16 at 14:30
  • Yes, I can do that, but how can I tell the difference of the service's onCreate being called by startService (from my activity) or by the fired alarm? There are three scenarios: 1. Call to onCreate from activity for the very first time => getService is null => start the alarm (fine) 2. Call to onCreate from activity later on => getService is not null => alarm should not be cancelled and data check should not run 3. Call to onOncreate from fired alarm => getService is not null => alarm should be cancelled and data check should run. How can I distinguish between 2 and 3? – devnull69 Feb 04 '16 at 14:41
  • You could put an extra on the Intent to distinguish. For example, when the Activity starts the Service, `intent.putExtra("from_activity", true);`. Then, in the Service, check `intent.getBooleanExtra("from_activity", false);`, which will return false as the default value (second argument) if the Intent doesn't have the extra. – Mike M. Feb 04 '16 at 14:48
  • This is really starting to offend me: I don't have access to the intent from inside onCreate in the service. Only `onStartCommand` can get the intent, but it is only called after onCreate and only when invoked from the activity (using startService) ... I start to get out of options here – devnull69 Feb 04 '16 at 15:47
  • Well, no matter where you call `new Intent(this, CheckDaysMonths.class)`, it's going to be the same thing, as far as `PendingIntent` is concerned, so you could just instantiate it anew wherever you need to. However, the cleanest way to do it would be to declare `private Intent serviceIntent;` as a field in the Service, and initialize it in `onCreate()`; i.e., `serviceIntent = new Intent(this, CheckDaysMonths.class)`. Then it will be accessible throughout the Service class. – Mike M. Feb 04 '16 at 15:59

1 Answers1

1

I fixed it by introducing another "middle tier" service

So now I have

  • The app GUI starting service 1
  • Service 1 will check if there is already an alarm with the correct signature running. If not, it will start an alarm
  • The alarm will fire at 2am and start service 2
  • Service 2 will check the data (SharedPreferences) and eventually send a notification. Then it will start the alarm again
  • A BroadcastReceiver for BOOT starts Service 2

This works quite well

devnull69
  • 16,402
  • 8
  • 50
  • 61