Monday, February 10, 2014

Joda Time Android issues

Everybody who was working with standard Java date, calendar and time api knows, how painful it could be with all these "umbrella" getters and setters like Calendar.set(int field, int amount), where you can set almost everything using appropriate params. Official documentation that recommends things like Calendar.get(Calendar.YEAR) - 1900 instead of Date.getYear() looks terrible as well.

After all of that, the idea to use Joda Time for date and time computing looks great - you got well designed and intuitive api with immutable classes (mutable versions are available too). The design of library makes you think in right terms like timezones, intervals, periods, durations and so on.

But this article isn't about advantages of Joda time. I would like to describe some issues that I faced when decided to use it in one of my android projects.

1. Slow loading (appropriate stackoverflow question). If you care about your app responsiveness (and I hope you do), you can notice that the very first Joda time class invocation can cause some visible lag of your app. In my case the lag was about 800 ms on old 2.3.6 devices like Nexus One. So I decided to call Joda initialization manually in background thread right after my app launch:

 // first joda time call can take time (100-900 ms on Nexus One), lets do  
 // it here in background thread for better performance  
 LocalTime.now().getMillisOfDay();  

It looks not very obvious, so don't forget to write some comments there.

2. Timezone changing. You might get surprised when you change your device timezone. Joda caches java's timezone and doesn't update it on system timezone change automatically. So you'll probably get logs with incorrect GMT offsets (as minimum) or incorrect result of LocalTime to millis conversion. So what you'll want to do in this case is to implement BroadcastReceiver that receives ACTION_TIMEZONE_CHANGED and manually sets default timezone to joda. Something like this (don't forget to register it in AndroidManifest.xml):

 public class TimezoneChangedReceiver extends BroadcastReceiver {  
     private static final String TAG = TimezoneChangedReceiver.class.getSimpleName();  
      
     @Override  
     public void onReceive(final Context context, final Intent i) {  
          String action = i.getAction();  
          Logger.d(TAG, "Going to handle action " + action);  
          if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {  
               Logger.d(TAG, "Going to update joda time zone!");  
               DateTimeZone.setDefault(DateTimeZone.forTimeZone(TimeZone.getDefault()));  
          }  
     }  
}  

3. Outdated timezones on some old devices. This is the most annoying issue I faced. Legacy devices like Nexus One with Android 2.3.6 have outdated timezones. Here in Minsk we have Further-eastern european time since September 2011 (see wiki) that is always UTC+03:00, so called "year-round daylight saving time". But it was Eastern european time (UTC+2 and UTC+3 with DST) until 2011. The same thing happened with Moscow standard time.

Nexus One doesn't know anything about further-eastern european time, but Joda Time library does, cause it uses its internal tz database with updated info about timezones. So the problem is that some functions will work unexpectedly for user. For example, if you call "LocalTime.now()", you might get time that differs from time that is displayed by device, cause the same unix-time means different local time in different timezones. Although Joda Time does all computations correctly, you might want to get offsets from the device, not from the updated tz-database. To do this you need to implement your own zone info provider (org.joda.time.tz.Provider inheritor). My implementation looks like this:

 /**  
  * This {@link Provider} is used for legacy devices (like Nexus One with 2.3.6)  
  * that have outdated timezones. {@link AndroidZoneInfoProvider} retrieves  
  * timezone offsets from device, not from updated tz-data. Although these  
  * timezones are incorrect, they are expectable for user and don't confuse him.  
  *   
  * For newer devices you probably don't want to use this class.  
  *   
  * @author Andrei Buneyeu  
  *   
  */  
 public class AndroidZoneInfoProvider implements Provider {  
      @SuppressWarnings("unused")  
      private static final String TAG = AndroidZoneInfoProvider.class.getSimpleName();  
      @Override  
      public DateTimeZone getZone(String id) {  
           final TimeZone timezone = TimeZone.getTimeZone(id);  
           if ("UTC".equalsIgnoreCase(id)) {  
                return DateTimeZone.UTC;  
           }  
           return new DateTimeZone(id) {  
                @Override  
                public long previousTransition(long instant) {  
                     return instant;  
                }  
                @Override  
                public long nextTransition(long instant) {  
                     return instant;  
                }  
                @Override  
                public boolean isFixed() {  
                     return !timezone.useDaylightTime();  
                }  
                @Override  
                public int getStandardOffset(long instant) {  
                     return timezone.getOffset(instant);  
                }  
                @Override  
                public int getOffset(long instant) {  
                     return timezone.getOffset(instant);  
                }  
                @Override  
                public String getNameKey(long instant) {  
                     return timezone.getDisplayName(Locale.US);  
                }  
                @Override  
                public boolean equals(Object object) {  
                     if (!(object instanceof DateTimeZone))  
                          return false;  
                     DateTimeZone dtz = (DateTimeZone) object;  
                     return dtz.getID().equals(getID());  
                }  
           };  
      }  
      @Override  
      public Set<String> getAvailableIDs() {  
           return new HashSet<String>(Arrays.asList(TimeZone.getAvailableIDs()));  
      }  
 }  

Of course there is no any guarantee that this implementation is absolutely bug-free (I'm not sure about correct implementation of nextTransition and prevTransition methods), but it seems to work properly. If you have any clarifications about that, feel free to post it in comments.