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.




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 будет только развиваться.

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

Friday, November 25, 2011

Motorola DroidX - useless HDMI port


http://www.androidpolice.com/2010/07/06/droid-x-hdmi-out-is-crippled-to-play-only-videos-or-display-photos-taken-on-the-device/
http://techcrunch.com/2010/08/30/motorola-droid-xs-hdmi-port-is-only-active-in-gallery-app/

Инженерам Motorola DroidX - пламенный привет. HDMI-выход в этом телефоне работает только тогда, когда вы запустите приложение "Галерея". Пустить изображение-видео из любого другого приложения на экран телевизора не получится. Нет слов.
Еще надо заказчику это объяснить.

Monday, October 24, 2011

Затемнение ImageView по нажатию

Недавно разобрался с такими типами Drawable, как LayerDrawable и StateListDrawable.
Для начала опишу задачу. Есть GridView, каждая ячейка которого представляет собой картинку 80x80. Картинка получается с сервера. Не так давно заказчик захотел, чтобы ячейка представляла собой не просто картинку, а картинку + круговую тень поверх нее, примерно вот такую, чтобы приложение смотрелось немного "гламурнее":
Кроме того, при нажатии на картинку картинка должна была еще затемняться одноцветной черной полупрозрачной маской.
Как это реализовать?
Вручную программно выполнять какие-то наложения картинок не захотелось сразу. Почти сразу решил сделать 3 картинки во FrameLayout, лежащих одна на другой, но после небольшой попытки выполнять соответствующий setVisibility на полупрозрачную маску по событию MotionEvent от этого тоже захотелось отказаться.
Вспомнились селекторы, которые позволяют указать соответствующий drawable элементу в зависимости от своего state - pressed, focused, и др. Но на свойство visibility нельзя повесить никакой selector, что, в принципе, логично. Поиск привел меня к LayerDrawable, которые позволяют создавать один Drawable по слоям из нескольких. Идея свелась к тому, чтобы использовать selector, в котором на pressed state подключать LayerDrawable с тремя вложенными картинками, на обычный state - с двумя.
Однако - сами-то картинки (не декоративные, а информационные) каждый раз разные! Поэтому на чистом xml, при всем желании, реализовать бы все не удалось, поэтому я плюнул, и решил все реализовать программно с использованием уже знакомых LayerDrawable и StateListDrawable (который может реализовать selector прямо в коде).
Получившийся код:
public class ObscuredImageView extends ImageView {
    protected static final String TAG = "ObscuredImageView";
    private Drawable _innerShading;
    private Drawable _obscured;

    public ObscuredImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        _innerShading = getContext().getResources().getDrawable(R.drawable.inner_shading);
        _obscured = getContext().getResources().getDrawable(R.color.semitransparent);
    }

    @Override
    public void setImageBitmap(Bitmap bitmap) {
        Log.v(TAG, "Going to set state drawable to ImageView");
        BitmapDrawable image = new BitmapDrawable(getContext().getResources(), bitmap);
        Drawable pressed = new LayerDrawable(new Drawable[] { image, _innerShading, _obscured });
        Drawable normal = new LayerDrawable(new Drawable[] { image, _innerShading });
        StateListDrawable states = new StateListDrawable();
        states.addState(new int[] { android.R.attr.state_pressed }, pressed);
        states.addState(new int[] { android.R.attr.state_focused }, pressed);
        states.addState(new int[] {}, normal);
        setImageDrawable(states);
    }
}

Решение вызывать setImageDrawable в переопределенном методе setImageBitmap на первый взгляд выглядит неудачно и "грязно", но если мы заглянем в исходный код класса ImageView, то мы увидим следующее:
public void setImageBitmap(Bitmap bm) {
    // if this is used frequently, may handle bitmaps explicitly
    // to reduce the intermediate drawable object
    setImageDrawable(new BitmapDrawable(bm));
}
Так что вызов setImageDrawable абсолютно корректен.

Таким образом, я получил работающее и гибкое решение. Если кто-то реализовал подобное другим образом - с интересом приму к сведению.

Friday, October 21, 2011

Meld Diff Viewer

Не так давно открыл для себя отличную программу под Linux для сравнения файлов и директорий - Meld Diff Viewer. Под Mac OS X, как написано здесь , тоже есть билд, правда, не знаю, насколько он симпатичный по сравнению с билдом из более "родной" среды.
Скриншот:
Уверен, каждый программист найдет ей применение.

Wednesday, October 19, 2011

Обновление ADT 14


ADT был обновлен до 14 версии, и сразу вылезла куча ошибок, связанных с использование R-ресурсов проекта-библиотеки.

Теперь R-файл библиотек содержит не константы, а обычные целые числа, что, в определенных случаях, позволяет избежать некоторых трудностей, как, например, переопределение какой-нибудь константы R.id и быстрее компилировать сложные проекты. Однако, использовать R.id в конструкциях switch-case не получится, т.к. в case обязаны лежать только константы. К счастью, Eclipse позволяет автоматически преобразовать switch-case в if-else по Ctrl+1 (Cmd+1 на маках).

Подробнее можно посмотреть здесь: http://tools.android.com/tips/non-constant-fields

UPD 21.10.2011. Если бы изменения касались только R-ресурсов :-( Навскидку за последние 48 часов, проблемы, с которыми столкнулся лично я:
http://groups.google.com/group/android-developers/browse_thread/thread/9findViewById93dc13262b84441/2c9d0f52e8a16bc1?lnk=raot
http://code.google.com/p/android/issues/detail?id=21031 (UPD 24.10.2011 - на эту проблему получен ответ от Tech Lead for the Android SDK at Google https://groups.google.com/group/adt-dev/msg/c1f4d73072f8b0e8)
UPD 27.10.2011. http://code.google.com/p/android/issues/detail?id=21162
http://code.google.com/p/android/issues/detail?id=21048
Так что обновляться пока никому посоветовать нельзя. А мне - урок, что незачем обновляться сразу после выхода обновления, а если обновляться, то аккуратно и с возможностью бысрого бэкапа.
UPD 28.10.2011. Вышел ADT 15, который должен исправлять многие проблемы. Попробую обновиться через пару дней.

Tuesday, August 30, 2011

Правила хорошего тона

В этом посте я постараюсь собрать правила хорошего тона при Android-разработке.
Все сразу вспомнить все равно не удастся, поэтому пункты еще долго будут добавляться.

1. Константы, используемые как имена (а такжы и как возможные значения) для данных, передаваемых через Intent, должны начинаться с INTENT_. Использовать данные из Intent нужно только один раз в методе onCreate(). Все это позволяет программисту сразу видеть, какие данные передаются или могут передаваться в Activity. Мне не нравится, когда данные, передаваемые фактически как входные аргументы, инициализируются в середине работы Activity непонятно в каком методе.

2. Переопределять методы onCreate(), onResume(), onDestroy() и прочие методы класса Activity следует как protected, вызываться из других классов они не должны. Также не следует вручную обращаться к методам, начинающимся с "on", таким, как onActivityResult() и другим. Я никогда не догадаюсь смотреть Call Hierarchy какого-либо из этих методов.

3. Все свойства (SharedPreferences) приложения должны считываться и записываться через статические методы get-set одного специального класса (который можно назвать, например, Preferences). Так мы сможем, во-первых, всегда видеть, какие в принципе свойства приложения устанавливаются, во-вторых, анализировать в любой момент, откуда какое свойство считывается или записывается.

4. Забудьте про такой xml-атрибут объекта View, как android:onClick. Он нарушает все возможные правила по разделению интерфейса и логики.
Когда я просматриваю код Activity, я ожидаю, что я буду видеть всю логику в этом же файле. Когда я вижу незнакомый метод неочевидного назначения, который не вызывает ни один другой метод, я его со спокойной совестью удалю (или сначала закомментирую на недельку).

5. Подавляющее большинство слушателей (listeners) должны быть реализованы как поля класса с говорящими именами. Если я хочу изменить поведение кнопки button1, то я не хочу пролистывать весь код, чтобы найти место, в котором кнопке назначается какой-то сразу реализованный слушатель. Я хочу пролистать список полей класса, и увидеть что-то вроде button1Listener. Исключения:
1) Логика нескольких слушатетелей достаточно тесно переплетается. В этом случае может быть удобно создать отдельный listener сразу для нескольких элементов (и распознавать элементы через передаваемый аргумент View v).
2) В некоторых случаях слушателем может быть назначено само Activity. На мой взгляд, это уместно, если назначение метода слушателя очевидно внутри Activity. Например, когда весь Activity фактически представляет из себя ListView, то реализация самим Activity интерфейсов OnItemClickListener или OnItemLongClickListener вполне логична. Назначать же какой-нибудь OnSeekBarChangeListener для какого-то ползунка в углу экрана на целое Activity смысла не имеет и путает программиста.

6. Если обращение к какому-то View-элементу происходит из Activity не один раз, то его нужно выносить в поле класса с инициализацией и назначением всех слушателей в onCreate() (или отдельном методе, например, initUI(), вызываемом из onCreate()) сразу после setContentView(). Так можно сразу увидеть, какие графические элементы активны, т. е. связаны с логикой приложения, а какие - нет. findViewById(int) внутри произвольного метода недопустимо.

7. Все создаваемые диалоги должны храниться в полях класса, а в методе onDestroy() для каждого диалога должен вызываться метод dismiss(). Примерно так:

@Override
protected void onDestroy() {
    if (progressDialogGettingData != null) {
        progressDialogGettingData.dismiss();
        progressDialogGettingData = null;
    }
    super.onDestroy();
}

Иначе, если Activity по каким-то причинам закрылось (например, при перевороте девайса), а диалог нет, и через несколько секунд поток, работающий с этим диалогом, вместе с завершением работы попытается закрыть и диалог, то приложение ждет force close. Мелочь, а неприятно. Конечно, в файле манифеста у Activity может быть установлена одна ориентация, или запрет закрытия Activity при перевороте, тогда закрыть Activity при открытом not cancellable диалоге будет затруднительно, но точно сказать, как поменяется манифест, нельзя, а такая конструкция в любом случае нам никак не повредит.

8. Элементы раскладки, не контролируемые из кода, не должны иметь аттрибута android:id вовсе. Это позволит легко, глядя на xml-файл, различать активные и пассивные элементы.

9. Не писать в лог в геттерах, а также в циклах. Если очень надо, то ставить минимальный уровень приоритета (verbose), чтобы кучу дежурных сообщений можно было легко отфильтровать. Когда нужно выловить действительно важное сообщение, а лог пополняется сообщениями со стандартным приоритетом с завидной скоростью - это действительно раздражает.

Sunday, August 28, 2011

Определение ориентации экрана

Установил на VirtualBox x86-версию Android, чтобы отладка не была мученьем (об этом потом). Установил разрешение 1024х600, запустил отлаживаемую программу. Приложение вело себя соответственно портретному режиму.
В коде обнаружилось следующее:
public static boolean isLandscapeMode(Context context) {
    Display display = ((WindowManager) (context.getSystemService(Context.WINDOW_SERVICE))).getDefaultDisplay();
    return display.getRotation() == Surface.ROTATION_90 || display.getRotation() == Surface.ROTATION_270;
}

В документации нашлось, что метод getRotation() returns the rotation of the screen from its "natural" orientation. В общем-то, все понятно, что Android внутри виртуальной машины воспринимает свою ориентацию как "натуральную", не думая о, собственно, разрешении экрана. На телефонах вроде моего любимого HTC ChaCha этот метод также должен работать неправильно. Заменил на решение "в лоб":

public static boolean isLandscapeMode(Context context) {
    Display display = ((WindowManager) (context.getSystemService(Context.WINDOW_SERVICE))).getDefaultDisplay();
    return display.getWidth() >= display.getHeight();
}

Все работает.

Thursday, August 11, 2011

А знаете ли вы, что...


Чтобы удалить подпункт options menu при его подготовке к отображению (в методе onPrepareOptionsMenu(Menu menu)), нужно сделать следующее:
menu.findItem(resourceMenuId).setVisible(false);
Стандартный вызов
menu.removeItem(resourceMenuId);
не сработает!

Sunday, August 7, 2011

Замена ContextMenu в Android

Эта статья - переведенное подведение итогов к вопросам http://stackoverflow.com/questions/6950586/how-could-i-customize-context-menu-item-click-callback и http://stackoverflow.com/questions/6958622/how-to-create-dialog-that-looks-exactly-like-contextmenu .

Контекстное меню на настольных ПК - это меню, вызываемое при нажатии правой кнопки мыши. Контекстное меню используется для того, чтобы предоставить пользователю выбор действия, которые надо совершить над элементом, чье контекстное меню выбрано.
В Android контекстное меню (класс ContextMenu) вызывается нажатием и удержанием графического элемента.

Чтобы контекстное меню было вызвано по нажатию на View, необходимо зарегистрировать в Activity методом registerForContextMenu(View). Все, что делает этот метод - вызывает View.setOnCreateContextMenuListener(OnCreateContextMenuListener), при чем Activity реализовывает интерфейс OnCreateContextMenuListener, так что как единственный параметр он передает ссылку на себя.

За создание контекстного меню отвечает метод Activity onCreateContextMenu (ContextMenu menu, View v,  ContextMenu.ContextMenuInfo menuInfo), который вызывается каждый раз при долгом нажатии на View v, а при выборе пункта контекстного меню вызывается метод onContextItemSelected (MenuItem item). Все просто.

Но тут возникают сложности.
Предположим, в нашем Activity есть несколько View, каждое из которых вызывает контекстное меню. Причем у каждого View контекстное меню совершенно разное, и поведение контекстного меню тоже разное. Тем не менее, обработку действий мы должны проводить в одном методе onContextItemSelected(MenuItem). Не очень логично реализовывать действия контекстного меню над, скажем, картинкой и элементом списка в одном методе, не так ли?
Конечно, можно перенаправиться в метод класса того элемента, чье контекстное меню вызывалось, где и будет реализовываться основная логика, но суть от этого не меняется.

Кроме того, в View должна храниться ссылка на Activity. Вроде бы мелочь, но заводить отдельное поле для того, чтобы зарегистрировать контекстное меню, которое из Activity перенаправится обратно во View не очень-то логично.

Изначально я хотел использовать кое-где ContextMenu, где это возможно, а кое-куда вставить хак - диалоговое окно, выглядящее и ведущее себя как ContextMenu. Оказалось, что да, во внутреннем пакете Android com.android.internal лежат необходимые классы, генерирующие ContextMenu посредством того же диалогового окна. Но копировать необходимые системные layout, недоступные извне, в свой проект мне показалось глупым - где гарантия, что какой-то из производителей Android-фонов не решит слегка изменить внешний вид контекстного меню?

Поэтому я решил не использовать ContextMenu вообще. Вместо контекстного меню можно использовать диалоговое окно (AlertDialog). Класс AlertDialog.Builder предоставляет несколько удобных методов для построения диалогового окна, среди которых самый важный в данном случае - setItems(CharSequence[], DialogInterface.OnClickListener) (также массив строк можно передавать через указатель на ресурс, например, R.array.foo), который создает ListView, адаптер к нему, используя, опять же, какую-то внутреннюю layout, и обработчик (подробнее - здесь). На вид получается точно такое же контекстное меню. Кроме того, сразу на месте можно создать listener, который обработает действия на месте, без перенаправления в другие классы. Таким образом, мы можем реализовывать логику в тех местах, в которых нам это необходимо, а внешний вид диалогов будет одинаковым везде.