вторник, 24 мая 2011 г.

Android: передача объектов между Activity

В одном из предыдущих постов мы затронули тему перехода между Activity в Android-приложениях. И, в частности, обсуждали передачу данных из одного Activity в другое. Очевидное решение  intent.putExtra(NextActivity.FIELD_NAME, field_value) хорошо служит нам до тех пор, пока мы передаём данные "простых" типов: String, int, long или массивы. Но стоит нам попытаться передать таким образом объект из нашей модели данных, начинаются трудности.
Объекты таким способом передавать можно, но они должны реализовывать интерфейс Serializable или Parcelable. Первый вариант по некоторым найденным мной отзывам, снижает производительность приложения. Второй - давайте рассмотрим внимательнее.
Вот пример класса одного из моих приложений:

  1. public class Email implements Parcelable {
  2.  
  3.   private String address;
  4.   private String type;
  5.  
  6.   private Email(Parcel in) {
  7.     this.address = in.readString();
  8.     this.type = in.readString();
  9.   }
  10.  
  11.   public String getAddress() {
  12.     return address;
  13.   }
  14.  
  15.   public void setAddress(String address) {
  16.     this.address = address;
  17.   }
  18.  
  19.   public String getType() {
  20.     return type;
  21.   }
  22.  
  23.   public void setType(String t) {
  24.     this.type = t;
  25.   }
  26.  
  27.   public Email(String a, String t) {
  28.     this.address = a;
  29.     this.type = t;
  30.   }
  31.  
  32.   public int describeContents() {
  33.     return 0;
  34.   }
  35.   public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
  36.  
  37.     public Email createFromParcel(Parcel in) {
  38.       return new Email(in);
  39.     }
  40.  
  41.     public Email[] newArray(int size) {
  42.       return new Email[size];
  43.     }
  44.   };
  45.  
  46.   public void writeToParcel(Parcel parcel, int i) {
  47.     parcel.writeString(address);
  48.     parcel.writeString(type);
  49.   }
  50. }
Как видим,он реализует интерфейс Parcelable, а значит может легко передаваться между Activity. Как мы этого добиваемся? Мы дополняем логику класса методами для его Parcel-изации и Де-parcel-изации, проще говоря, сохранения в Parcel и восстановления из него. Parcel тут можно рассматривать как некий буфер, в который можно сложить в определённом порядке данные любых типов и затем (в том же порядке !) их оттуда извлечь.
Складывать содержимое полей класса в Parcel очень просто. Это реализуется в методе writeToParcel. Для восстановления объекта действуем так: Создаём вложенный класс Parcelable.Creator, в котором реализуем два метода. Второй выглядит всегда одинаково, а вот первый для нас весьма важен: он вызовет новый конструктор нашего класса, передавая в него Parcel. В конструкторе мы должны реализовать логику, обратную методу writeToParcel, т.е. вычитать из Parcel-а значения полей класса в том же порядке, в каком их туда записывали. В случае простых полей типа String это несложно. Если же у нас есть поля типа ArrayList, то делаем так:
p.writeInt(phone.size());
for (int i = 0; i < phone.size(); i++) {
  p.writeString(phone.get(i));
}
для сохранения, и
int count = p.readInt();
for (int i = 0; i < count; i++) {
  String ph = p.readString();
  phone.add(ph);
}
для восстановления. Тут p, как вы, наверное, догадались - объект класса Parcel, а phone - поле типа ArrayList.
Если полями нашего класса являются другие наши классы, то мы должны их также "научить Parcel-изоваться".  Тогда поля таких типов мы будем укладывать в Parcel методом
p.writeParcelable(organization, Parcelable.CONTENTS_FILE_DESCRIPTOR);
и извлекать оттуда методом
organization = p.readParcelable(getClass().getClassLoader());

воскресенье, 22 мая 2011 г.

Redis: пишем Java-клиент

В одном из прежних постов я описывал свои впечатления от знакомства с Redis. Тогда я использовал библиотеку Jedis. А недавно мне попалось описание протокола Redis-сервера и я решил поупражняться в его освоении. Чтобы было нагляднее я "завернул" результат в простое swing-приложение, которое можно скачать на моём сайте. Итак...

Коротко о протоколе
Для соединения с сервером используем tcp-протокол, порт по умолчанию - 6379. Команды и наборы данных в ответах завершаются последовательностью "\r\n" (CRLF). Первый символ ответа указывает на тип возвращаемого значения. Тут ":" - число, "-" - сообщение об ошибке, "+" - однострочный ответ. Интереснее ответы, начинающиеся с "$". Это, так называемые "составные" ответы. Они содержат две строки, первая из которых - число байт, которые нужно прочитать во второй. Такие ответы будем получать наиболее часто, их возвращает команда GET для обычных ключей. Если же ключ необычный, например хэш, то ответ будет "многосоставной". В нём первый символ - "*", затем число результатов в ответе, затем результаты, каждый по две строке в виде составных ответов, начинающихся с "$".

Архитектура
В последней (на сегодня) версии Redis я насчитал более 120 команд. Из них в обычной практике понадобится не более десятка, но хорошая библиотека должна легко расширяться.
Схема работы будет такой: каждая команда будет представлена одним классом, в нём - специфическая логика по формированию запроса и интерпретации ответа. Сам метод, который пишет/читает данные будет реализован в их общем предке. Все классы будут реализовывать один и тот же абстрактный метод. Приложение о классах команд ничего знать не должно, для него у нас будет Factory, которое будет принимать запрос, подгружать в рантайме нужный класс, создавать его инстанс, инициализировать его (например отдавать ему уже открытый сокет) и дёргать его стандартный метод. В коде это выглядит так:

  1. public class RedisFactory {
  2.   
  3.   private Socket s;
  4.   public static final String COMMAND_AUTH = "redisui.connector.command.Auth";
  5.   public static final String COMMAND_KEYS = "redisui.connector.command.Keys";
  6.   public static final String COMMAND_GET = "redisui.connector.command.Get";
  7.   public static final String COMMAND_TYPE = "redisui.connector.command.Type";
  8.   public static final String COMMAND_HGETALL = "redisui.connector.command.Hgetall";
  9.   public static final String COMMAND_DEL = "redisui.connector.command.Del";
  10.   
  11.   public void connect(String host, String port, String pass) throws Exception {
  12.     String[] params = {host, port, pass};
  13.     exec(COMMAND_AUTH, params);
  14.   }
  15.   
  16.   public Response exec(String cmd, String[] params) throws Exception {
  17.     Class<? extends CommandBased> command = Class.forName(cmd).asSubclass(CommandBased.class);
  18.     CommandBased ci = command.newInstance();
  19.     ci.setS(this.s);
  20.     Response r = ci.exec(params);
  21.     this.s = ci.getS();
  22.     return r;
  23.   } 
  24. }
Так, во имя стандартизации мы параметры в запрос передаём как массив строк, а ответ получаем в виде такого себе объекта-оборотня Response:

  1. public class Response {
  2.  
  3.   private String type;
  4.   private byte[] out;
  5.   public static final String TYPE_LINE = "+";
  6.   public static final String TYPE_ERROR = "-";
  7.   public static final String TYPE_INT = ":";
  8.   public static final String TYPE_STRING = "$";
  9.   public static final String TYPE_ARRAY = "*";
  10.  
  11.   public Response(String type, byte[] out) {
  12.     this.type = type;
  13.     this.out = out;
  14.   }
  15.  
  16.   public String getLineOut() {
  17.     ...
  18.   }
  19.   
  20.   public int getIntOut() {
  21.     ...
  22.   }
  23.   
  24.   public String getStringOut() {
  25.     ...
  26.   }
  27.  
  28.   public String[] getArrayOut() {
  29.     ...
  30.   }
  31.  
  32.   public String getType() {
  33.     return type;
  34.   }
  35. }
На стороне приложения мы либо знаем, какого типа будет ответ и в таком случае вызываем нужный getter, либо обрабатываем все варианты, получая тип конкретного ответа из getType().
И, наконец, пример класса команды:

  1. public class Get extends CommandBased {
  2.  
  3.   @Override
  4.   public Response exec(String[] params) throws Exception {
  5.     String command = "get "+params[0]+"\r\n";
  6.     Response res = interrupt(command);
  7.     if (res.getType().equals(Response.TYPE_ERROR)) {
  8.       throw new Exception(res.getLineOut());
  9.     }
  10.     return res;
  11.   }
  12. }
Как видим, добавлять новые команды в такую библиотеку - одно удовольствие.

Результат:
За пару часов работы получился прототип, который ищет ключи по маске, позволяет просматривать их содержимое и удалять их.
По мере добавления функционала буду выкладывать новые версии у себя на сайте. Если кому пригодится - всегда на здоровье :).

среда, 11 мая 2011 г.

Android: список операций для элемента списка

Часто в Android-приложении приходится решать одну и ту же задачу: вывести список и дать возможность пользователю выполнить набор действий с каждым элементом этого списка. На днях задался целью сделать для себя универсальное решение.

Принцип работы списка будет таким:

  1. Создаём ListView и на событие OnItemClick вешаем слушатель (назовём его, к примеру Operation).
  2. В методе обработчике слушателя строим диалоговое окно со списком операций. Список операций в виде String[] и заголовок окна передаём в Operation при вызове конструктора слушателя. 
  3. На событие клика по названию операции в диалоговом окне вешаем этот же слушатель. Для этого наш класс Operation должен реализовывать не только интерфейс OnItemClickListener но и DialogInterface.OnClickListener. 
  4. Во втором методе-обработчике (onClick) выполняем операцию над выбранным элементом нашего ListView. Для этого объект выбранного элемента нужно получить в первом методе-обработчике (onItemClick) и передать во второй конструктор Operation. 
Схема может показаться запутанной, но исходник, приведенный ниже, думаю, поможет разобраться:

  1. import android.app.Activity;
  2. import android.app.AlertDialog;
  3. import android.content.DialogInterface;
  4. import android.view.View;
  5. import android.widget.AdapterView;
  6. import android.widget.AdapterView.OnItemClickListener;
  7.  
  8. public class Operation implements DialogInterface.OnClickListener, OnItemClickListener {
  9.   
  10.   private Activity act;
  11.   private Object obj;
  12.   private String title;
  13.   private String[] oper_list;
  14.  
  15.   public Operation(Activity act, String title, String[] oper_list) {
  16.     this.act = act;
  17.     this.title = title;
  18.     this.oper_list = oper_list;
  19.   }
  20.  
  21.   public Operation(Activity act, Object obj) {
  22.     this.act = act;
  23.     this.obj = obj;
  24.   }
  25.   
  26.   public void onClick(DialogInterface di, int i) {
  27.     if (i==0) {
  28.       // TODO: first oheration with obj
  29.     }
  30.     // TODO: next operations with obj
  31.   }
  32.  
  33.   public void onItemClick(AdapterView<?> av, View view, int i, long l) {
  34.     Object curr = av.getItemAtPosition(i);
  35.     AlertDialog.Builder builder = new AlertDialog.Builder(act);
  36.     builder.setTitle(title);
  37.     builder.setItems(oper_list, new Operation(act, curr));
  38.     AlertDialog alert = builder.create();
  39.     alert.show();
  40.   }
  41. }
А в том месте, где нужно построить список поступаем просто:
    ListView lw = new ListView(this);
    String[] operations = {"Edit", "Delete"};
    lw.setAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, items));
    lw.setOnItemClickListener(new Operation(this, "Operations", operations));

воскресенье, 8 мая 2011 г.

Android: переход между окнами в приложении

Давайте проведём небольшую аналогию. Допустим мы умеем делать сайты, а хотим научиться писать приложения под Android. Казалось бы, мало общего... Но это как посмотреть. Большая часть интерфейсов так или иначе подразумевают переход между окнами (страницами) приложения (сайта). Динамичные web-интерфейсы сводят переход между страницами к минимуму, подменяя отдельные части контента ajax-ом. А там, где это целесообразно, меняем страницу целиком. Есть аналогичный подход в Andriod-приложении? Конечно.
Каждое окно Android-приложения представлено классом, расширяющим Activity. В этом классе изначально нет "контента". Заполняем его методом setContentView(View v), где v - корневой элемент иерархии контейнеров и виджетов, каждый из которых также наследует View. Ничего не  напоминает? Activity похож на Document, а построение иерархии элементов с помощью addView() похоже на appendChild() при построении dom-дерева в javascript. Не правда ли? Теоретически, любое Android-приложение можно построить на базе одного единственного Activity, подменяя в нужный момент его содержимое или часть его с помощью setContentView. Это похоже на чисто ajax-овый сайт из одной страницы. И даже недостатки такого подхода кое-где похожи. На таком сайте бесполезна кнопка "назад" в браузере, а в таком приложении бесполезна аппаратная кнопка "назад" устройства.  Конечно, это поправимо и для сайта и для Android-приложения, но вопрос не в этом. Рассмотрим второй подход. Допустим, мы решили сделать "честное" приложение, где каждому экрану интерфейса соответствует одно Activity. Поступаем примерно так:
Intent intent = new Intent();
intent.setClass(this, NextActivity.class);
startActivity(intent);

Тут NextActivity представляет экран к которому мы переходим. Несколько сложнее, чем в web, конечно. Теперь посмотрим на передачу данных. У каждого Activity своя область видимости переменных, как и у каждой страницы для javascript. Переходя к другому Activity мы теряем возможность использовать поля предыдущего. Данные нужно передавать из одного окна в другое также как и из одной страницы на другую, используя посредника. В случае web, это сервер, который принимает данные из запроса и строит на основании их новую страницу. В случае Android-приложения это среда исполнения, которая по сути делает то же самое.
Тут становится понятна роль Intent. Это что-то вроде запроса. В него мы "вкладываем" команды и данные, и из него потом получаем страницу результата. Происходит это примерно так. В первом Activity:
Intent intent = new Intent();
intent.setClass(this, NextActivity.class);
intent.putExtra(NextActivity.FIELD_NAME, field_value);
startActivity(intent);

А во втором, соответственно, получаем данные из Intent:

  1. public class NextActivity extends Activity {
  2.     public static final String FIELD_NAME = "field_name";
  3.     
  4.     @Override
  5.     public void onCreate(Bundle icicle) {
  6.         super.onCreate(icicle);
  7.         String field_value = getIntent().getExtras().getStirng(FIELD_NAME);
  8.         Toast.makeText(this, "field value is "+field_value, Toast.LENGTH_SHORT).show();
  9.     }
  10. }
Вот так мы передаём строку из первого окна и отображаем её во втором. Первое окно при этом уходит "в фон", а кнопка "назад" в любой момент может вернуть ему управление. При этом данные второго окна останутся недоступными, как и в случае с web-страницами. А чтобы передать данные обратно нужна снова помощь "посредника". В общем, по моему, аналогия есть. Конечно, как и все аналогии она не точна. Но этого и не требуется. Главное в том, что с её помощью легче понять устройство Android-приложений.

пятница, 6 мая 2011 г.

Mouse tracking system своими руками

Что это и зачем оно нам?
Есть много систем, которые предоставляют подробную статистику по использованию сайта. "Всемирно известный" Google Analytics, к примеру, расскажет сколько посетителей было, откуда, на каких страницах, когда, как быстро они ушли. При небольшой настройке можно посчитать какой процент из них нажал на кнопку "Оплатить" и т.п. Опций - великое множество, нет (или пока нет) разве что mouse tracking-а. Это опция отслеживания перемещений курсора. Статистика "закурсоривания" страницы позволяет визуально оценить какие области пользуются наибольшим вниманием среднего посетителя, а какие - наоборот. Работает психологический принцип: курсор чаще всего следует за взглядом.
Из сервисов, предоставляющих такую статистику нельзя не отметить clicktale.com и русскоязычный аналог - webvizor. Оба они записывают действия посетителей на страницах (перемещение курсора, клики и скроллинг) а затем предоставляют "тепловые карты" страниц и видеозаписи. Оба сервиса платные, причём стоимость зависит от числа страниц, которые нужно наблюдать, объёма данных которые нужно собрать и времени, в течении которого эти данные будут доступны для анализа. Если нужен постоянный мониторинг большого портала - это влетит в копейку. И не всегда удобно держать на своих страницах чужой скрипт, который следит за посетителями. То есть нам-то удобно, но посетителям, скорее всего, не понравится. Особенно, если наш сервис, к примеру, платёжная система.
Так может не ждать нам "милостей от природы", а взять и сделать такой сервис самостоятельно?

Оценим требования
  1. Нужно анализировать все страницы сайта (порядка 150). 
  2. Нужно следить за перемещением курсора (с допустимой погрешностью, к примеру, 10 пикселей)
  3. Скроллинг не учитываем: 80% страниц - небольшие формы, которые не требуют прокрутки
  4. Посетителей пишем всех. Это порядка 500 тыс. в сутки
  5. Данные храним все, отчёты будем выбирать за произвольный период.
  6. При проблемах с нагрузкой допускается потеря до 30% данных. Отличная вещь статистика :)
Проблемы, которые прийдётся решить
Обьём данных. 
Хранить каждое перемещение мыши каждого пользователя нереально. Альтернатива - храним матрицы вида {x, y, counter} для каждой страницы. Для удобства анализа и построения карт все "экраны посетителей" приведём к разрешению 800 x 600. Итого получаем максимальное число записей в базе: 800*600*150 = 72 миллиона. В реальности 2/3 площади страниц будет не покрыто "вниманием" посетителей, так что можно ориентироваться на цифру в 24 миллиона. В любом случае объёмы серьёзные. Будем сливать в архив данные за период (допустим за неделю) и очищать рабочую таблицу для нового "отчётного периода".
Трафик.
document.body.onmousemove = function() { ... ajax ... } убьёт наш сервис на первом же тестовом прогоне. Решения два, и используем мы их оба. Первое: очевидное - пишем точки, отстоящие друг от друга на n пикселей. Чем меньше n, тем больше точность и трафик. Сохранение данных также делаем с учётом точности. Ищем точку, у которой |x-x0| < n && |y-y0| < n и наращиваем счётчик у неё вместо создания новой записи. Очевидно, что при наличии нескольких близких точек, берём ближайшую. Второе решение: "собираем" точки на клиентской стороне и отправляем пакетом. Тут мы теряем неполный пакет, когда посетитель уходит со страницы, но это допустимая жертва на алтарь борьбы с трафиком. Понятно, что размер пакета и точность можно подбирать для достижения максимального качества при допустимой нагрузке.


Состав системы
Для сбора данных много усилий не потребуется: js - скрипт на сотню строк и серверная часть. Реализуем её позже, с ней всё просто: получить пакет, слить в базу с учётом точности.
Для отображения постраничных "карт внимания" понадобится закрытая область системы с авторизацией. Поскольку сервис внутренний, особенно изобретать тут ничего не понадобится. В аккаунте менеджер сайта должен видеть скриншоты страниц с наложенными на них матрицами отображающими усреднённый путь курсора клиентов. Где взять скриншоты? Можно, конечно же воспользоваться одном из сторонних сервисов, которые позволяют выгрузить скрин по url любой страницы, но мы так делать не будем. Во-первых страницы наши - закрытые, а во вторых зачем нам опять сторонние сервисы? Пока достаточно чтобы менеджер сам залил скрин страницы в разрешении 800*600. Тем более что вначале все страницы и не понадобятся. Серверную часть реализуем на java (servlet + jsp). Отрисовку карт внимания поверх скринов страниц сделаем javascript-ом.

JavaScript-модуль
  1. var tracker = {};
  2.  
  3. tracker.accur = 10;
  4. tracker.pX = 0;
  5. tracker.pY = 0;
  6. tracker.req;
  7. tracker.cashe = new Array();
  8.  
  9. // дожидаемся загрузки страницы и вешаем обработчик перемещения курсора
  10. tracker.initOnLoad = function() {
  11.     if(document.body != null && typeof(document.body) != "undefined") {
  12.         document.onmousemove = tracker.trackEvent;
  13.     }
  14.     else {
  15.         setTimeout(function() {
  16.             tracker.initOnLoad();
  17.         }, 1);
  18.     }    
  19. }
  20.  
  21. // обрабатываем перемещение курсора
  22. tracker.trackEvent = function(e) {
  23.     var e=e || window.event
  24.     var mX=e.x || e.clientX
  25.     var mY=e.y || e.clientY
  26.     
  27.     var wX = document.body.clientWidth || document.body.innerWidth
  28.     var wY = document.body.clientHeight || document.body.innerHeight
  29.  
  30.     mX = (mX/wX*800).toFixed(0);
  31.     mY = (mY/wY*600).toFixed(0);
  32.  
  33.     if ((mX-tracker.pX)>tracker.accur || (mY-tracker.pY)>tracker.accur) {
  34.         tracker.pX = mX;
  35.         tracker.pY = mY;
  36.         tracker.addToCashe(mX, mY);
  37.         tracker.sendCashe();
  38.     }    
  39. }
  40.  
  41. // добавляем точку в пакет
  42. tracker.addToCashe = function(x, y) {
  43.     var ncashe = new Array();
  44.     var found = false;
  45.     for(var i=0; i<tracker.cashe.length; i++) {
  46.         var m = tracker.cashe[i];
  47.         if (m.x==x && m.y==y) {
  48.             ncashe[ncashe.length] = {x:m.x, y:m.y, counter:(m.counter+1)};
  49.             found = true;
  50.         } else {
  51.             ncashe[ncashe.length] = m;
  52.         }
  53.     }
  54.     if (!found) {
  55.         ncashe[ncashe.length] = {x:x, y:y, counter:1};
  56.     }
  57.     tracker.cashe = ncashe;    
  58. }
  59.  
  60. // отправляем пакет на сервер
  61. tracker.sendCashe = function() {
  62.     if(tracker.cashe.length>49) {
  63.         var data = "";
  64.         for(var i=0; i<tracker.cashe.length; i++) {
  65.             var m = tracker.cashe[i];
  66.             data += m.x+"t"+m.y+"t"+m.counter+"n";
  67.         }
  68.         data = data.substr(0, (data.length-1));
  69.         var url = "http://your.server.side/servlet?&a="+tracker.accur+"&data="+data+"&url="+encodeURIComponent(window.location.href.substr(0, 100));
  70.         tracker.sendTrackData(url);
  71.         tracker.cashe = new Array();
  72.     }
  73. }
  74.  
  75. // ajax запрос. Ответ нам не нужен, его не обрабатываем
  76. tracker.sendTrackData = function(url) {
  77.     if (window.XMLHttpRequest) {
  78.         try {
  79.             tracker.req = new XMLHttpRequest();
  80.         } catch (e){}
  81.     } else if (window.ActiveXObject) {
  82.         try {
  83.             tracker.req = new ActiveXObject('Msxml2.XMLHTTP');
  84.         } catch (e){
  85.             try {
  86.                 tracker.req = new ActiveXObject('Microsoft.XMLHTTP');
  87.             } catch (e){}
  88.         }
  89.     }
  90.     if (tracker.req) {     
  91.         tracker.req.open("GET", url, true);
  92.         tracker.req.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
  93.         tracker.req.send(null);
  94.     }
  95. }
  96.  
  97. // ну а тут всё начинается :)
  98. tracker.initOnLoad();

Сорри за стиль, давно не брался за Javascript. Но в целом, должно быть понятно. В следующих постах опишу серверную часть и реализацию админки.
По затратам времени: не отвлекаясь от основной работы прототип удалось запустить за два вечера. Под нагрузкой прийдётся "доводить" ещё дня три.  Как видим, трудозатраты совершенно не сопоставимы с выгодами и преимуществами, которые получаем от такого сервиса.