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.




Thursday, May 16, 2013

Android Studio v0.1

Great news! Android Studio - a new Android IDE based on IntelliJ IDEA - was presented by Google about 14 hours ago.

NetBeans in variety of versions was my first IDE  probably (do you remember Java ME Platform SDK 3.0 that was actually NetBeans with deeply built-in plugins for J2ME?), but when I saw Eclipse I was impressed its power and expandability.

Eclipse is a great universal IDE that can do almost anything I want and I'll use it right now in projects after posting this text.

However, it's probably too universal to provide convenient ways for specific android developer actions. Layout editor built-in Eclipse was always slow and not very representative. It was always simpler and faster for me to edit xml-layouts manually. And slowness is not related only to Eclipse's layout editor, yeah. Eclipse is slow everywhere.

Hope we'll get something new and amazing with new Android Studio! It's on "early access preview" state now, so perhaps it's better to finish your current projects (and next one too) in Eclipse, but don't forget to check Studio's updates and look what's going on with it!

P.S. ADT was updated to 22.0 version too!

Thursday, June 28, 2012

ffmpeg causes app killing by Signal 7 (SIGBUS)

A Hacker's Craic: Slow queues and big maths
http://stackoverflow.com/questions/10739368/sigbus-crash-only-on-samsung-galaxy-s2/11244741#11244741


I had very similar issue - ffmpeg caused crash of my app by signal 7 (SIGBUS) after decoding several hundreds of frames. It was not very device specific - on some devices app crashed more often, on other less often.

It was very incomprehensible issue for me cause it could happen at any time of video encoding/decoding and I couldn't find any conditions that causes failures.
But I recompiled ffmpeg with `-malign-double` flag according to this useful article:
http://software.intel.com/en-us/blogs/2011/08/18/understanding-x86-vs-arm-memory-alignment-on-android/. And it helped! So my full "configure" line is (of course not all of these flags are useful for you):

> ./configure --target-os=linux --cross-prefix=arm-linux-androideabi-
> --disable-asm --arch=arm --cpu=cortex-a9 --sysroot=${SYSROOT} --enable-neon --disable-avdevice --enable-decoder=mjpeg --enable-demuxer=mjpeg --enable-parser=mjpeg --enable-demuxer=image2 --enable-muxer=mp4  --prefix=build/armeabi-v7a --extra-cflags='-DANDROID -I${NDK_ROOT}/sources/cxx-stl/system/include -mtune=cortex-a9 -mfpu=neon -mfloat-abi=softfp' --extra-ldflags='-Wl,--fix-cortex-a8 -malign-double -L../android-libs -Wl,-rpath-link,../android-libs' --extra-cxxflags='-Wno-multichar -malign-double -fno-exceptions -fno-rtti'

Hope it'll help you too.

Wednesday, May 23, 2012

xhdpi

Не забывайте про xhdpi, как про ресурсы в папке drawable-xdpi (мне было бы неприятно видеть картинки в мыле на своем самом продвинутом телефоне), так и про внутренние вычисления в коде, если такие имеются. Все последние телефоны (такие как, например, HTC One X) имеют сверхвысокую плотность точек. Нам было досадно узнать, что на новых девайсах наше приложение работает не так как нужно, поскольку в нашей арифметике случая xhdpi просто не было предусмотрено.

Monday, April 2, 2012

synchronized(Boolean)


Читал чужой код, вижу блоки синхронизации по какой-то переменной, и вдруг понимаю, что тип этой переменной - Boolean. То есть смысл переменной в том, чтобы хранить какой-то флажок, а объектом переменная сделана для того, чтобы иметь возможность выполнить по ней синхронизацию. И тут мы проходим мимо здоровенных граблей, который могут нас больно ударить по самому больному месту. Дело в том, что существует всего два объекта Boolean - Boolean.FALSE и Boolean.TRUE. В двух абсолютно не связанных друг с другом кусках кода мы можем иметь по переменной типа Boolean. Эти переменные будут иметь абсолютно разный смысл, но в один момент они могут ссылаться на один объект. И когда мы войдем в блок синхронизации в одном месте программы, другой блок уже не сможет выполниться. И наоборот. Хотя логической связи между ними не будет вообще.

Формально, конечно, Boolean не синглетон. Конструктор у него public, и экземпляров может быть сколько угодно, то есть вместо Boolean.valueOf() мы можем использовать new Boolean(). Но это выглядит как еще больший хак - любой программист заменит конструктор на статический метод Boolean.valueOf(), как рекомендует официальная документация, и незаслуженно получит в лоб. Я не могу придумать ситуацию, где мог бы понадобиться конструктор Boolean и синхронизация по нему.

Как вывод - НИКОГДА не используйте синхронизацию для объектов типа Boolean и, обобщая, для оберток всех примитивных типов (поскольку некоторые объекты могут кэшироваться внутри класса).

Tuesday, December 27, 2011

ListView сохранение положения


Хочется зафиксировать элегантный способ решения проблемы. Есть ListView, отображающий файлы и папочки, по которым можно перемещаться. Заказчик пожаловался, что при возврате на уровень выше (по аппаратной кнопке back) положение списка должно сохраняться. То есть если мы находились на середине списка, где щелкнули по какой-то папочке, то потом мы должны снова оказаться в середине списка.
Первой попыткой наугад было просто сохранить в стеке (в котором хранилось содержимое каждой папочки уровня выше текущего) Y-координату. Сохранить по getScrollY(), восстановить через scrollTo(O, y). Однако обнаружилось, что getScrollY() - это метод View, а не ListView, и соответственно, возвращает он значение, немного другое по смыслу.
Обнаружился соответствующий вопрос на stackoverflow. Выигрышным ответом было выполнить следующее:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// ...
// restore
mList.setSelectionFromTop(index, top);

Решения понятное, но слишком уж низкоуровневое. Чуть ниже оказался ответ всего с одним плюсом (уже с двумя):
// Save ListView state
Parcelable state = listView.onSaveInstanceState();
// Restore previous state (including selected item index and scroll position)
listView.onRestoreInstanceState(state);

Вот и все! Вызовы всех более низкоуровневых методов ListView осуществляет уже у себя внутри. Все крайне очевидно и понятно.
Мораль такова, что всегда нужно пытаться мыслить более абстрактно, и не забывать про удобный метод View.onSaveInstanceState().

Wednesday, December 14, 2011

ADT 16

На днях ADT обновился до 16-й версии. Самое значительное изменение - появление Lint, утилиты, которая анализирует Android-проект и выявляет потенциальные баги, находит типичные ошибки раскладок (layouts), неиспользуемые ресурсы.

Раньше для поиска неиспользуемых ресурсов приходилось пользоваться сторонними утилитами (как, например, android-unused-resources), удобство пользования которыми, признаемся, оставляло желать лучшего. Я даже думал написать свою собственную утилиту (отчасти для себя, отчасти для того, чтобы получить плюс в карму за создание и поддержку полезного проекта с открытым исходным кодом :)), не взялся отчасти и от того, что ожидал чего-то подобного от Google, а инициатива от Google убила бы проект на корню. Дождался!

Полноценного анализа java-кода пока не производится, как и не производится анализ связей между проектами (например, ресурсы могут не использоваться внутри библиотечного проекта, но использоваться другими проектами в том же workspace), но, следует полагать, Lint будет только развиваться.

Всем удачного обновления и приятного дня!