Friday, September 12, 2014

Foreground notification launches "App Info" screen

Recently I found one issue with Notifications that can be simply formulated as "Don't forget to use setSmallIcon while building a notification".

Otherwise, you won't get any notification if you launch it from NotificationManager. That's bad and not obvious at all, but still easily noticeable.

But if you show your notification (without called setSmallIcon) via Service.startForeground() , you'll get notification with not working PendingIntent - on click you'll see your "App Info" settings screen. setSmallIcon fixed the problem immediately. This behavior is completely not understandable for me, but I happy I fixed it.

I spent several hours to isolate the issue (just by many attempts to show notification different ways), so I hope it helps someone.

Wednesday, July 2, 2014

A little tip about equals() and hashCode()


Don't forget to check if equals() and hashCode() are overriden in any class (unless you use String or class that you're very familiar with) you use in collection!

And a little story about that...

Several days ago I tried to fix a memory leak somewhere in LruCache. LruCache provides a way to cache some objects while they're not exceeding a particular amount of memory. After that the oldest object in the cache is not being hold any more and available for garbage collector.

LruCache isn't a collection itself, but it should contain a map inside (and it does, have a look at the source code here) for key to value mapping (where the value is a cached object you need), so any class you use as key for LruCache should contain overridden equals() and hashCode().

But, unlucky for me, I used ResolveInfo as the key (and application icon as a value). Although it looks like a pretty simple data structure, it doesn't contain equals() and hashCode() overridden, I don't see the reason why - you can look here if you don't believe me.

So instead of caching it simply stored my icons and sometimes garbage collector couldn't do anything before OutOfMemoryError was throwed.

In my case, I replaced ResolveInfo to my own class that contains two fields: resolveInfo.activityInfo.packageName and resolveInfo.activityInfo.name and ultimately override equals() and hashCode(). Pair class is useful for such purposes too (and I actually copied equals() and hashCode() implementation from there) but it's always better to use more meaningful names.

Have a nice day!

P.S. Be careful to update to ADT 23.0.0 - it ruined my Eclipse and I needed to download the whole ADT Bundle for clean install. See here for details.

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.