I want to extend a discussion I started on the Reddit Android Dev community yesterday with a new question: How do you manage an up-to-date timezone database shipped with your app using the JodaTime library on a device that has outdated timezone information?
The problem
The specific issue at hand relates to a particular timezone, "Europe/Kaliningrad". And I can reproduce the problem: On an Android 4.4 device, if I manually set its time zone to the above, calling new DateTime()
will set this DateTime
instance to a time one hour before the actual time displayed on the phone's status bar.
I created a sample Activity
to illustrate the problem. On its onCreate()
I call the following:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ResourceZoneInfoProvider.init(getApplicationContext());
ViewGroup v = (ViewGroup) findViewById(R.id.root);
addTimeZoneInfo("America/New_York", v);
addTimeZoneInfo("Europe/Paris", v);
addTimeZoneInfo("Europe/Kaliningrad", v);
}
private void addTimeZoneInfo(String id, ViewGroup root) {
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
am.setTimeZone(id);
//Joda does not update its time zone automatically when there is a system change
DateTimeZone.setDefault(DateTimeZone.forID(id));
View v = getLayoutInflater().inflate(R.layout.info, root, false);
TextView idInfo = (TextView) v.findViewById(R.id.id);
idInfo.setText(id);
TextView timezone = (TextView) v.findViewById(android.R.id.text1);
timezone.setText("Time zone: " + TimeZone.getDefault().getDisplayName());
TextView jodaTime = (TextView) v.findViewById(android.R.id.text2);
//Using the same pattern as Date()
jodaTime.setText("Time now (Joda): " + new DateTime().toString("EEE MMM dd HH:mm:ss zzz yyyy"));
TextView javaTime = (TextView) v.findViewById(R.id.time_java);
javaTime.setText("Time now (Java): " + new Date().toString());
root.addView(v);
}
ResourceZoneInfoProvider.init()
is part of the joda-time-android library and it is meant to initialize Joda's time zone database. addTimeZoneInfo
overwrites the device's time zone and inflates a new view where the updated time zone information is displayed. Here is an example of result:
Note how for "Kaliningrad", Android maps it to "GMT+3:00" because that was the case until 26 October 2014 (see Wikipedia article). Even some web sites still show this time zone as GMT+3:00 because of how relatively recent this change is. The correct, however, is "GMT+2:00" as displayed by JodaTime.
Flawed possible solutions?
This is a problem because no matter how I try to circumvent it, in the end, I have to format the time to display it to the user in their time zone. And when I do that using JodaTime, the time will be incorrectly formatted because it will mismatch the expected time the system is displaying.
Alternatively, suppose I handle everything in UTC. When the user is adding an event in the calendar and picks a time for the reminder, I can set it to UTC, store it in the db like that and be done with it.
However, I need to set that reminder with Android's AlarmManager
not at the UTC time I converted the time set by the user but at the one relative to the time they want the reminder to trigger. This requires the time zone info to come into play.
For example, if the user is somewhere at UTC+1:00 and he or she sets a reminder for 9:00am, I can:
- Create a new
DateTime
instance set for 09:00am at the user's timezone and store its milliseconds in the db. I can also directly use the same milliseconds with theAlarmManager
; - Create a new
DateTime
instance set for 09:00am at UTC and store its milliseconds in the db. This better addresses a few other issues not exactly related to this question. But when setting the time with theAlarmManager
, I need to compute its millisecond value for 09:00am at the user's timezone; - Ignore completely Joda
DateTime
and handle the setting of the reminder using Java'sCalendar
. This will make my app rely on outdated time zone information when displaying the time but at least there won't be inconsistencies when scheduling with theAlarmManager
or displaying the dates and times.
What am I missing?
I may be over thinking this and I'm afraid I might be missing something obvious. Am I? Is there any way I could keep using JodaTime on Android short of adding my own time zone management to the app and completely disregard all built-in Android formatting functions?
You're doing it wrong. If the user adds a calendar entry for 9 am next year he means 9 am. It doesn't matter if the timezone database in the user's device changes or the government decides to alter the start or end of daylight savings time. What matters is what clocks say on the wall. You need to store "9 am" in your database.
Since you are using
AlarmManager
, and it would appear that it requires you to set the time in UTC, then you're correct in that you will need to project your time to UTC in order to schedule it.But you don't have to persist it that way. For example, if you have a recurring daily event, then store the local time of day that the event should fire. If the event is to occur in a specific time zone (rather than the current time zone of the device), then store that time zone ID also.
Project the local time to the UTC time using JodaTime - then pass that value to
AlarmManager
.Periodically (or at minimum, whenever you apply an update to JodaTime's data), re-evaluate the scheduled UTC times. Cancel and re-establish the events as necessary.
Watch out for times scheduled during a DST transition. You may have a local time that's not valid on a specific day (which should probably be advanced), or you may have a local time which is ambiguous on a specific day (which you should probably pick the first of the two instances).
Ultimately, I think your concern is that it would be possible for the Joda Time data to be more accurate than the device's data. Thus, an alarm might go off at the correct local time - but it might not match the time showing on the device. I agree that it might be puzzling for the end-user, but they'll probably be thankful that you did the right thing. I don't think there's much you can do about it anyway.
One thing you might consider is to check
TimeUtils.getTimeZoneDatabaseVersion()
and compare it against the version number of the data you loaded into Joda Time. If they are out of sync, you could present a warning message to your user to either update your application (when you are behind), or update their device's tzdata (when the device is behind).A quick search found these instructions and this app for updating the tzdata on Android. (I have not tested them myself.)
You need to override Joda Time, especially Timezone Provider, and use system's timezones instead of IANA database. Let's show an example of DateTimezone which is based on system's TimeZone class:
Also I have created a fork of Joda Time, which is uses the system's timezones, it is available here. As a bonus it has less weight without timezones database.
Couple of words what be the best approach: Use the LocalDate and LocalDateTime classes to store and calculate reminders' time. But before calling the alarmManager.setExact method, convert it to the DateTime which acounts default timezone. But surround it with the try-catch operator as the IllegalInstantException could happen. Because there could be no such time that day. In the catch block, adjust the time according to the DST shift.
This is generally an impossible problem. You can never store delta time(e.g. ms after the epoch) for a future event at a given wall clock time, and be confident it will remain correct. Consider that for any arbitrary time, a nation may sign a law that daylight savings time goes into effect 30 minutes before that time, and time skips forward by 1 hour. A solution which may mitigate this problem is, instead of asking the phone to wake up at a specific time, wake up periodically, check the current time, and then check if any reminders should be activated. This could be done efficiently by adjusting the wake up time to reflect an approximate idea of when the next reminder is set for. For example, if the next reminder isn't for 1 year, wake up after 360 days, and start waking up more frequently until you are very close to the time of the reminder.
I think the other answers are missing the point. Yes, when persisting time information, you should consider carefully your use cases to decide how best to do so. But even if you had done it, the problem this question poses would still persist.
Consider Android's alarm clock app, which has its source code freely available. If you look at its
AlarmInstance
class, this is how it is modeled in the database:And to know when an alarm instance should fire, you call
getAlarmTime()
:Note how an
AlarmInstance
stores the exact time it should fire, regardless of time zone. This ensures that every time you callgetAlarmTime()
you get the correct time to fire on the user's time zone. The problem here is if the time zone is not updated,getAlarmTime()
cannot get correct time changes, for example, when DST starts.JodaTime comes in handy in this scenario because it ships with its own time zone database. You could consider other date time libraries such as date4j for the convenience of better handling date calculations, but these typically don't handle their own time zone data.
But having your own time zone data introduces a constraint to your app: you cannot rely anymore on Android's time zone. That means you cannot use its
Calendar
class or its formatting functions. JodaTime provides formatting functions as well, use them. If you must convert toCalendar
, instead of using thetoCalendar()
method, create one similar to thegetAlarmTime()
above where you pass the exact time you want.Alternatively, you could check whether there is a time zone mismatch and warn the user like Matt Johnson suggested in his comment. If you decide to keep using both Android's and Joda's functions, I agree with him:
Except there is one more thing you can do: You can change Android's time zone yourself. You should probably warn the user before doing so but then you could force Android to use the same time zone offset as Joda's:
After checking, if it is not the same, you can change Android's time zone with a "fake" zone you create from the offset of Joda's correct time zone information:
Remember you will need the
<uses-permission android:name="android.permission.SET_TIME_ZONE"/>
permission for that.Finally, changing the time zone will change the system current time. Unfortunately only system apps can set the time so the best you can do is open the date time settings for the user and prompt him/her to change it manually to the correct one:
You will also have to add some more controls to make sure the time zone gets updated when DST starts and ends. Like you said, you will be adding your own time zone management but it's the only way to ensure the consistency between the two time zone databases.