1

I'm scheduling a repeating alarm, ringing every Tuesday morning, at 08:30.

Because of Doze, I can't use AlarmManager.setRepeating because the time of the alarm won't be precise (it needs to be, I understand the effects on battery).

So, I use this code to schedule the first alarm :

Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.setFirstDayOfWeek(Calendar.WEDNESDAY); // Trick not to get a "tuesday" timestamp in the past if today is wednesday+ !
calendar.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY);
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 30);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
long scheduledTime = calendar.getTimeInMillis();

AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, scheduledTime, pendingIntent);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    alarmManager.setExact(AlarmManager.RTC_WAKEUP, scheduledTime, pendingIntent);
} else {
    alarmManager.set(AlarmManager.RTC_WAKEUP, scheduledTime, pendingIntent);
}

And it works flawlessly.

Problem is, I have to re-schedule the alarm when the Intent in consumed by my BroadcastReceiver, on Tuesday, 8:30AM.

But between Tuesday 08:30 and Tuesday 23:59, the method will give me a timestamp in the "past", which is incorrect.

Is there any method than "if the provided timestamp is in the past, add 1 week to it" to fix this issue?

Nino DELCEY
  • 652
  • 1
  • 9
  • 25
  • Sorry, but I don't understand why you can't calculate the next scheduling date/time when rescheduling the alarm. – Juan Jun 24 '17 at 13:09
  • Because the first part of the code (line 0 - 8) will give me a timestamp that is before now ? – Nino DELCEY Jun 24 '17 at 13:13
  • 1
    Compare `if (calculated.before(now)) {...` – Lew Bloch Jun 24 '17 at 15:15
  • @LewBloch That's what I'm doing at the moment, but it feels so inelegant, I'm looking for a more "generic" approach. – Nino DELCEY Jun 24 '17 at 15:31
  • "Feels" is an emotion. What is the engineering rationale? Anyway, you're already setting the initial value of `calendar` to the current time twice, which is "inelegant" by engineering standards. You might consider using the https://docs.oracle.com/javase/8/docs/api/java/util/Calendar.html#add-int-int- method if you are sticking with the old-fashioned time manipulation library. – Lew Bloch Jun 24 '17 at 20:14

3 Answers3

4

I would suggest going for the new java.time API for this calculation. The ThreeTenABP project is an adaptation of that library for Android specifically.

Here is an example of getting 08:30 AM of every Tuesday using java.time API:

LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault());
now.with(TemporalAdjusters.next(DayOfWeek.TUESDAY))
   .withHour(8)
   .withMinute(30)
   .withSecond(0));  
Pallavi Sonal
  • 3,661
  • 1
  • 15
  • 19
2

The old classes (Date, Calendar and SimpleDateFormat) have lots of problems and design issues, and they're being replaced by the new APIs.

In Android you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes, together with the ThreeTenABP (more on how to use it here).

Although you can also use Joda-Time, it's in maintainance mode and being replaced by the new APIs, so I don't recommend start a new project with it. Even in joda's website it says: "Note that Joda-Time is considered to be a largely “finished” project. No major enhancements are planned. If using Java SE 8, please migrate to java.time (JSR-310).".

All the classes below are under the package org.threeten.bp.


First of all, I've checked the documentation of AlarmManager and it seems to work with timestamps (the long parameter in set methods), which is the number of milliseconds from 1970-01-01T00:00Z.

So, you need a ZonedDateTime (a date and time in a specific timezone), because you need to know the date, time and the timezone to get a specific timestamp millis:

import org.threeten.bp.DayOfWeek;
import org.threeten.bp.LocalTime;
import org.threeten.bp.ZonedDateTime;
import org.threeten.bp.temporal.TemporalAdjusters;

// current date/time at system's default timezone
ZonedDateTime now = ZonedDateTime.now();

// get next Tuesday at 08:30 AM
ZonedDateTime nextTuesday = now
    // get the next Tuesday
    .with(TemporalAdjusters.next(DayOfWeek.TUESDAY))
    // at 08:30 AM
    .with(LocalTime.of(8, 30));
System.out.println(nextTuesday);

// get the millis timestamp
long scheduledTime = nextTuesday.toInstant().toEpochMilli();

The TemporalAdjusters.next() method does the trick of finding the next Tuesday. If the current date is already a Tuesday and you don't want to get the Tuesday of next week, you can replace it by TemporalAdjusters.nextOrSame().

The LocalTime.of(8, 30) sets the hour to 08:30 AM (it already sets the seconds and milliseconds to zero).


The code above gets the current date/time using the system's default timezone (probably the one configured in the device). If you want a specific timezone, you can use the org.threeten.bp.ZoneId or org.threeten.bp.ZoneOffset classes:

// current date/time at London
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Europe/London"));

// current date/time in UTC
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);

Note that the API uses IANA timezones names (always in the format Continent/City, like America/Sao_Paulo or Europe/London). Avoid using the 3-letter abbreviations (like CST or PST) because they are ambiguous and not standard.


If you already have a java.util.Calendar, you can convert from and to the new API using the org.threeten.bp.DateTimeUtils class:

// convert calendar to ZonedDateTime, using system's default timezone
ZonedDateTime z = DateTimeUtils.toInstant(calendar).atZone(ZoneId.systemDefault());

// convert back to calendar
Calendar c = DateTimeUtils.toGregorianCalendar(z);
0

You can calculate next Tuesday's 8:30 am with this code. It uses JodaTime Library.

At the time of this answer this is the dependancy to add to build.gradle: compile 'joda-time:joda-time:2.9.9'

public DateTime getNextTuesday830(DateTime now){
        DateTime rtn = now;

        int thisDay = now.getDayOfWeek();
        int deltaNexTuesday = -1;

        if(thisDay >= DateTimeConstants.TUESDAY){
            deltaNexTuesday =  DateTimeConstants.TUESDAY + 7 - thisDay;
        }else{
            deltaNexTuesday = DateTimeConstants.TUESDAY - thisDay;
        }

        rtn = rtn.plusDays(deltaNexTuesday).withHourOfDay(8).withMinuteOfHour(30).withSecondOfMinute(0);

        return rtn;
    }

To calculate the now parameter:

DateTime now = new DateTime(System.currentTimeMillis());

To get back a Date() object from DateTime:

Date d = now.toDate();
Juan
  • 5,525
  • 2
  • 15
  • 26
  • 2
    The Joda-Time project is now in maintenance mode, with the team advising migration to the java.time classes. See the *ThreeTen-Backport* and *ThreeTenABP* projects. – Basil Bourque Jun 24 '17 at 15:35
  • @BasilBourque Thx, I wasn't aware of that. – Juan Jun 25 '17 at 14:34